committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
119 changed files with 7957 additions and 753 deletions
-
165.github/workflows/DNS.yml
-
8.github/workflows/DragonFlyBSD.yml
-
8.github/workflows/FreeBSD.yml
-
83.github/workflows/Haiku.yml
-
2.github/workflows/Linux.yml
-
10.github/workflows/NetBSD.yml
-
8.github/workflows/Omnios.yml
-
8.github/workflows/OpenBSD.yml
-
79.github/workflows/OpenIndiana.yml
-
2.github/workflows/PebbleStrict.yml
-
12.github/workflows/Solaris.yml
-
2.github/workflows/dockerhub.yml
-
15.github/workflows/pr_dns.yml
-
1.github/workflows/pr_notify.yml
-
66.github/workflows/wiki-monitor.yml
-
21Dockerfile
-
452README.md
-
410acme.sh
-
2deploy/ali_cdn.sh
-
2deploy/ali_dcdn.sh
-
56deploy/cachefly.sh
-
86deploy/directadmin.sh
-
13deploy/docker.sh
-
86deploy/edgio.sh
-
2deploy/haproxy.sh
-
98deploy/kemplm.sh
-
131deploy/keyhelp.sh
-
86deploy/keyhelp_api.sh
-
276deploy/multideploy.sh
-
69deploy/netlify.sh
-
102deploy/panos.sh
-
130deploy/proxmoxbs.sh
-
12deploy/proxmoxve.sh
-
10deploy/qiniu.sh
-
4deploy/routeros.sh
-
28deploy/ruckus.sh
-
18deploy/strongswan.sh
-
8deploy/synology_dsm.sh
-
2deploy/truenas.sh
-
343deploy/truenas_ws.sh
-
93deploy/unifi.sh
-
98deploy/vault.sh
-
500deploy/zyxel_gs1900.sh
-
4dnsapi/dns_1984hosting.sh
-
160dnsapi/dns_active24.sh
-
17dnsapi/dns_ali.sh
-
2dnsapi/dns_aws.sh
-
15dnsapi/dns_azure.sh
-
281dnsapi/dns_beget.sh
-
2dnsapi/dns_bookmyname.sh
-
4dnsapi/dns_cf.sh
-
5dnsapi/dns_cloudns.sh
-
5dnsapi/dns_constellix.sh
-
4dnsapi/dns_curanet.sh
-
53dnsapi/dns_cyon.sh
-
2dnsapi/dns_ddnss.sh
-
2dnsapi/dns_dnshome.sh
-
2dnsapi/dns_duckdns.sh
-
2dnsapi/dns_dyn.sh
-
16dnsapi/dns_dynv6.sh
-
2dnsapi/dns_easydns.sh
-
163dnsapi/dns_edgecenter.sh
-
139dnsapi/dns_efficientip.sh
-
226dnsapi/dns_exoscale.sh
-
2dnsapi/dns_fornex.sh
-
2dnsapi/dns_freedns.sh
-
105dnsapi/dns_freemyip.sh
-
10dnsapi/dns_gandi_livedns.sh
-
45dnsapi/dns_he_ddns.sh
-
2dnsapi/dns_hetzner.sh
-
593dnsapi/dns_hetznercloud.sh
-
501dnsapi/dns_hostup.sh
-
244dnsapi/dns_infoblox_uddi.sh
-
15dnsapi/dns_inwx.sh
-
2dnsapi/dns_joker.sh
-
96dnsapi/dns_la.sh
-
18dnsapi/dns_limacity.sh
-
109dnsapi/dns_mgwm.sh
-
214dnsapi/dns_mijnhost.sh
-
10dnsapi/dns_myapi.sh
-
2dnsapi/dns_mydnsjp.sh
-
2dnsapi/dns_namecom.sh
-
2dnsapi/dns_namesilo.sh
-
62dnsapi/dns_nanelo.sh
-
6dnsapi/dns_netcup.sh
-
45dnsapi/dns_omglol.sh
-
1dnsapi/dns_openprovider.sh
-
186dnsapi/dns_openprovider_rest.sh
-
30dnsapi/dns_opnsense.sh
-
2dnsapi/dns_ovh.sh
-
2dnsapi/dns_pdns.sh
-
2dnsapi/dns_pleskxml.sh
-
216dnsapi/dns_qc.sh
-
18dnsapi/dns_rage4.sh
-
2dnsapi/dns_schlundtech.sh
-
447dnsapi/dns_selectel.sh
-
309dnsapi/dns_sotoon.sh
-
212dnsapi/dns_spaceship.sh
-
7dnsapi/dns_technitium.sh
-
2dnsapi/dns_tele3.sh
@ -0,0 +1,83 @@ |
|||
name: Haiku |
|||
on: |
|||
push: |
|||
branches: |
|||
- '*' |
|||
paths: |
|||
- '*.sh' |
|||
- '.github/workflows/Haiku.yml' |
|||
|
|||
pull_request: |
|||
branches: |
|||
- dev |
|||
paths: |
|||
- '*.sh' |
|||
- '.github/workflows/Haiku.yml' |
|||
|
|||
concurrency: |
|||
group: ${{ github.workflow }}-${{ github.ref }} |
|||
cancel-in-progress: true |
|||
|
|||
|
|||
|
|||
jobs: |
|||
Haiku: |
|||
strategy: |
|||
fail-fast: false |
|||
matrix: |
|||
include: |
|||
- TEST_ACME_Server: "LetsEncrypt.org_test" |
|||
CA_ECDSA: "" |
|||
CA: "" |
|||
CA_EMAIL: "" |
|||
TEST_PREFERRED_CHAIN: (STAGING) |
|||
- TEST_ACME_Server: "LetsEncrypt.org_test" |
|||
CA_ECDSA: "" |
|||
CA: "" |
|||
CA_EMAIL: "" |
|||
TEST_PREFERRED_CHAIN: (STAGING) |
|||
ACME_USE_WGET: 1 |
|||
#- TEST_ACME_Server: "ZeroSSL.com" |
|||
# CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" |
|||
# CA: "ZeroSSL RSA Domain Secure Site CA" |
|||
# CA_EMAIL: "githubtest@acme.sh" |
|||
# TEST_PREFERRED_CHAIN: "" |
|||
runs-on: ubuntu-latest |
|||
env: |
|||
TEST_LOCAL: 1 |
|||
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }} |
|||
CA_ECDSA: ${{ matrix.CA_ECDSA }} |
|||
CA: ${{ matrix.CA }} |
|||
CA_EMAIL: ${{ matrix.CA_EMAIL }} |
|||
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} |
|||
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} |
|||
steps: |
|||
- uses: actions/checkout@v4 |
|||
- uses: vmactions/cf-tunnel@v0 |
|||
id: tunnel |
|||
with: |
|||
protocol: http |
|||
port: 8080 |
|||
- name: Set envs |
|||
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV |
|||
- name: Clone acmetest |
|||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ |
|||
- uses: vmactions/haiku-vm@v1 |
|||
with: |
|||
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' |
|||
nat: | |
|||
"8080": "80" |
|||
prepare: | |
|||
mkdir -p /boot/home/.cache |
|||
pkgman install -y cronie |
|||
sync: rsync |
|||
copyback: false |
|||
run: | |
|||
cd ../acmetest \ |
|||
&& ./letest.sh |
|||
- name: onError |
|||
if: ${{ failure() }} |
|||
run: | |
|||
echo "See how to debug in VM:" |
|||
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" |
|||
|
|||
@ -0,0 +1,79 @@ |
|||
name: OpenIndiana |
|||
on: |
|||
push: |
|||
branches: |
|||
- '*' |
|||
paths: |
|||
- '*.sh' |
|||
- '.github/workflows/OpenIndiana.yml' |
|||
|
|||
pull_request: |
|||
branches: |
|||
- dev |
|||
paths: |
|||
- '*.sh' |
|||
- '.github/workflows/OpenIndiana.yml' |
|||
|
|||
concurrency: |
|||
group: ${{ github.workflow }}-${{ github.ref }} |
|||
cancel-in-progress: true |
|||
|
|||
|
|||
|
|||
jobs: |
|||
OpenIndiana: |
|||
strategy: |
|||
matrix: |
|||
include: |
|||
- TEST_ACME_Server: "LetsEncrypt.org_test" |
|||
CA_ECDSA: "" |
|||
CA: "" |
|||
CA_EMAIL: "" |
|||
TEST_PREFERRED_CHAIN: (STAGING) |
|||
- TEST_ACME_Server: "LetsEncrypt.org_test" |
|||
CA_ECDSA: "" |
|||
CA: "" |
|||
CA_EMAIL: "" |
|||
TEST_PREFERRED_CHAIN: (STAGING) |
|||
ACME_USE_WGET: 1 |
|||
#- TEST_ACME_Server: "ZeroSSL.com" |
|||
# CA_ECDSA: "ZeroSSL ECC Domain Secure Site CA" |
|||
# CA: "ZeroSSL RSA Domain Secure Site CA" |
|||
# CA_EMAIL: "githubtest@acme.sh" |
|||
# TEST_PREFERRED_CHAIN: "" |
|||
runs-on: ubuntu-latest |
|||
env: |
|||
TEST_LOCAL: 1 |
|||
TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }} |
|||
CA_ECDSA: ${{ matrix.CA_ECDSA }} |
|||
CA: ${{ matrix.CA }} |
|||
CA_EMAIL: ${{ matrix.CA_EMAIL }} |
|||
TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} |
|||
ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} |
|||
steps: |
|||
- uses: actions/checkout@v4 |
|||
- uses: vmactions/cf-tunnel@v0 |
|||
id: tunnel |
|||
with: |
|||
protocol: http |
|||
port: 8080 |
|||
- name: Set envs |
|||
run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV |
|||
- name: Clone acmetest |
|||
run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ |
|||
- uses: vmactions/openindiana-vm@v1 |
|||
with: |
|||
envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET' |
|||
nat: | |
|||
"8080": "80" |
|||
prepare: pkg install socat curl |
|||
sync: nfs |
|||
run: | |
|||
cd ../acmetest \ |
|||
&& ./letest.sh |
|||
- name: onError |
|||
if: ${{ failure() }} |
|||
run: | |
|||
echo "See how to debug in VM:" |
|||
echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM" |
|||
|
|||
@ -0,0 +1,66 @@ |
|||
name: Notify via Issue on Wiki Edit |
|||
|
|||
on: |
|||
gollum: |
|||
|
|||
jobs: |
|||
notify: |
|||
runs-on: ubuntu-latest |
|||
if: github.actor != 'neilpang' |
|||
steps: |
|||
- name: Checkout wiki repository |
|||
uses: actions/checkout@v4 |
|||
with: |
|||
repository: ${{ github.repository }}.wiki |
|||
path: wiki |
|||
fetch-depth: 0 |
|||
|
|||
- name: Generate wiki change message |
|||
run: | |
|||
actor="${{ github.actor }}" |
|||
sender_url=$(jq -r '.sender.html_url' "$GITHUB_EVENT_PATH") |
|||
page_name=$(jq -r '.pages[0].page_name' "$GITHUB_EVENT_PATH") |
|||
page_sha=$(jq -r '.pages[0].sha' "$GITHUB_EVENT_PATH") |
|||
page_url=$(jq -r '.pages[0].html_url' "$GITHUB_EVENT_PATH") |
|||
page_action=$(jq -r '.pages[0].action' "$GITHUB_EVENT_PATH") |
|||
page_summary=$(jq -r '.pages[0].summary' "$GITHUB_EVENT_PATH") |
|||
now="$(date '+%Y-%m-%d %H:%M:%S')" |
|||
|
|||
cd wiki |
|||
prev_sha=$(git rev-list $page_sha^ -- "$page_name.md" | head -n 1) |
|||
if [ -n "$prev_sha" ]; then |
|||
git diff $prev_sha $page_sha -- "$page_name.md" > ../wiki.diff || echo "(No diff found)" > ../wiki.diff |
|||
else |
|||
echo "(no diff)" > ../wiki.diff |
|||
fi |
|||
cd .. |
|||
{ |
|||
echo "Wiki edited" |
|||
echo -n "User: " |
|||
echo "@$actor [$actor]($sender_url)" |
|||
echo "Time: $now" |
|||
echo "Page: [$page_name]($page_url) (Action: $page_action)" |
|||
echo "Comment: $page_summary" |
|||
echo "[Click here to Revert](${page_url}/_history)" |
|||
echo "" |
|||
echo "----" |
|||
echo "### diff:" |
|||
echo '```diff' |
|||
cat wiki.diff |
|||
echo '```' |
|||
} > wiki-change-msg.txt |
|||
|
|||
- name: Create issue to notify Neilpang |
|||
uses: peter-evans/create-issue-from-file@v5 |
|||
with: |
|||
title: "Wiki edited" |
|||
content-filepath: ./wiki-change-msg.txt |
|||
assignees: Neilpang |
|||
env: |
|||
TZ: Asia/Shanghai |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
@ -0,0 +1,56 @@ |
|||
#!/usr/bin/env sh |
|||
|
|||
# Script to deploy certificate to CacheFly |
|||
# https://api.cachefly.com/api/2.5/docs#tag/Certificates/paths/~1certificates/post |
|||
|
|||
# This deployment required following variables |
|||
# export CACHEFLY_TOKEN="Your CacheFly API Token" |
|||
|
|||
# returns 0 means success, otherwise error. |
|||
|
|||
######## Public functions ##################### |
|||
|
|||
#domain keyfile certfile cafile fullchain |
|||
CACHEFLY_API_BASE="https://api.cachefly.com/api/2.5" |
|||
|
|||
cachefly_deploy() { |
|||
_cdomain="$1" |
|||
_ckey="$2" |
|||
_ccert="$3" |
|||
_cca="$4" |
|||
_cfullchain="$5" |
|||
|
|||
_debug _cdomain "$_cdomain" |
|||
_debug _ckey "$_ckey" |
|||
_debug _ccert "$_ccert" |
|||
_debug _cca "$_cca" |
|||
_debug _cfullchain "$_cfullchain" |
|||
|
|||
if [ -z "$CACHEFLY_TOKEN" ]; then |
|||
_err "CACHEFLY_TOKEN is not defined." |
|||
return 1 |
|||
else |
|||
_savedomainconf CACHEFLY_TOKEN "$CACHEFLY_TOKEN" |
|||
fi |
|||
|
|||
_info "Deploying certificate to CacheFly..." |
|||
|
|||
## upload certificate |
|||
string_fullchain=$(sed 's/$/\\n/' "$_cfullchain" | tr -d '\n') |
|||
string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n') |
|||
|
|||
_request_body="{\"certificate\":\"$string_fullchain\",\"certificateKey\":\"$string_key\"}" |
|||
_debug _request_body "$_request_body" |
|||
_debug CACHEFLY_TOKEN "$CACHEFLY_TOKEN" |
|||
export _H1="Authorization: Bearer $CACHEFLY_TOKEN" |
|||
_response=$(_post "$_request_body" "$CACHEFLY_API_BASE/certificates" "" "POST" "application/json") |
|||
|
|||
if _contains "$_response" "message"; then |
|||
_err "Error in deploying $_cdomain certificate to CacheFly." |
|||
_err "$_response" |
|||
return 1 |
|||
fi |
|||
_debug response "$_response" |
|||
_info "Domain $_cdomain certificate successfully deployed to CacheFly." |
|||
return 0 |
|||
} |
|||
@ -0,0 +1,86 @@ |
|||
#!/usr/bin/env sh |
|||
|
|||
# Script to deploy certificate to DirectAdmin |
|||
# https://docs.directadmin.com/directadmin/customizing-workflow/api-all-about.html#creating-a-login-key |
|||
# https://docs.directadmin.com/changelog/version-1.24.4.html#cmd-api-catch-all-pop-passwords-frontpage-protected-dirs-ssl-certs |
|||
|
|||
# This deployment required following variables |
|||
# export DirectAdmin_SCHEME="https" # Optional, https or http, defaults to https |
|||
# export DirectAdmin_ENDPOINT="example.com:2222" |
|||
# export DirectAdmin_USERNAME="Your DirectAdmin Username" |
|||
# export DirectAdmin_KEY="Your DirectAdmin Login Key or Password" |
|||
# export DirectAdmin_MAIN_DOMAIN="Your DirectAdmin Main Domain, NOT Subdomain" |
|||
|
|||
# returns 0 means success, otherwise error. |
|||
|
|||
######## Public functions ##################### |
|||
|
|||
#domain keyfile certfile cafile fullchain |
|||
directadmin_deploy() { |
|||
_cdomain="$1" |
|||
_ckey="$2" |
|||
_ccert="$3" |
|||
_cca="$4" |
|||
_cfullchain="$5" |
|||
|
|||
_debug _cdomain "$_cdomain" |
|||
_debug _ckey "$_ckey" |
|||
_debug _ccert "$_ccert" |
|||
_debug _cca "$_cca" |
|||
_debug _cfullchain "$_cfullchain" |
|||
|
|||
if [ -z "$DirectAdmin_ENDPOINT" ]; then |
|||
_err "DirectAdmin_ENDPOINT is not defined." |
|||
return 1 |
|||
else |
|||
_savedomainconf DirectAdmin_ENDPOINT "$DirectAdmin_ENDPOINT" |
|||
fi |
|||
if [ -z "$DirectAdmin_USERNAME" ]; then |
|||
_err "DirectAdmin_USERNAME is not defined." |
|||
return 1 |
|||
else |
|||
_savedomainconf DirectAdmin_USERNAME "$DirectAdmin_USERNAME" |
|||
fi |
|||
if [ -z "$DirectAdmin_KEY" ]; then |
|||
_err "DirectAdmin_KEY is not defined." |
|||
return 1 |
|||
else |
|||
_savedomainconf DirectAdmin_KEY "$DirectAdmin_KEY" |
|||
fi |
|||
if [ -z "$DirectAdmin_MAIN_DOMAIN" ]; then |
|||
_err "DirectAdmin_MAIN_DOMAIN is not defined." |
|||
return 1 |
|||
else |
|||
_savedomainconf DirectAdmin_MAIN_DOMAIN "$DirectAdmin_MAIN_DOMAIN" |
|||
fi |
|||
|
|||
# Optional SCHEME |
|||
_getdeployconf DirectAdmin_SCHEME |
|||
# set default values for DirectAdmin_SCHEME |
|||
[ -n "${DirectAdmin_SCHEME}" ] || DirectAdmin_SCHEME="https" |
|||
|
|||
_info "Deploying certificate to DirectAdmin..." |
|||
|
|||
# upload certificate |
|||
string_cfullchain=$(sed 's/$/\\n/' "$_cfullchain" | tr -d '\n') |
|||
string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n') |
|||
|
|||
_request_body="{\"domain\":\"$DirectAdmin_MAIN_DOMAIN\",\"action\":\"save\",\"type\":\"paste\",\"certificate\":\"$string_key\n$string_cfullchain\n\"}" |
|||
_debug _request_body "$_request_body" |
|||
_debug DirectAdmin_ENDPOINT "$DirectAdmin_ENDPOINT" |
|||
_debug DirectAdmin_USERNAME "$DirectAdmin_USERNAME" |
|||
_debug DirectAdmin_KEY "$DirectAdmin_KEY" |
|||
_debug DirectAdmin_MAIN_DOMAIN "$DirectAdmin_MAIN_DOMAIN" |
|||
_response=$(_post "$_request_body" "$DirectAdmin_SCHEME://$DirectAdmin_USERNAME:$DirectAdmin_KEY@$DirectAdmin_ENDPOINT/CMD_API_SSL" "" "POST" "application/json") |
|||
|
|||
if _contains "$_response" "error=1"; then |
|||
_err "Error in deploying $_cdomain certificate to DirectAdmin Domain $DirectAdmin_MAIN_DOMAIN." |
|||
_err "$_response" |
|||
return 1 |
|||
fi |
|||
|
|||
_info "$_response" |
|||
_info "Domain $_cdomain certificate successfully deployed to DirectAdmin Domain $DirectAdmin_MAIN_DOMAIN." |
|||
|
|||
return 0 |
|||
} |
|||
@ -0,0 +1,86 @@ |
|||
#!/usr/bin/env sh |
|||
|
|||
# Here is a script to deploy cert to edgio using its API |
|||
# https://docs.edg.io/guides/v7/develop/rest_api/authentication |
|||
# https://docs.edg.io/rest_api/#tag/tls-certs/operation/postConfigV01TlsCerts |
|||
|
|||
# This deployment required following variables |
|||
# export EDGIO_CLIENT_ID="Your Edgio Client ID" |
|||
# export EDGIO_CLIENT_SECRET="Your Edgio Client Secret" |
|||
# export EDGIO_ENVIRONMENT_ID="Your Edgio Environment ID" |
|||
|
|||
# If have more than one Environment ID |
|||
# export EDGIO_ENVIRONMENT_ID="ENVIRONMENT_ID_1 ENVIRONMENT_ID_2" |
|||
|
|||
# returns 0 means success, otherwise error. |
|||
|
|||
######## Public functions ##################### |
|||
|
|||
#domain keyfile certfile cafile fullchain |
|||
edgio_deploy() { |
|||
_cdomain="$1" |
|||
_ckey="$2" |
|||
_ccert="$3" |
|||
_cca="$4" |
|||
_cfullchain="$5" |
|||
|
|||
_debug _cdomain "$_cdomain" |
|||
_debug _ckey "$_ckey" |
|||
_debug _ccert "$_ccert" |
|||
_debug _cca "$_cca" |
|||
_debug _cfullchain "$_cfullchain" |
|||
|
|||
if [ -z "$EDGIO_CLIENT_ID" ]; then |
|||
_err "EDGIO_CLIENT_ID is not defined." |
|||
return 1 |
|||
else |
|||
_savedomainconf EDGIO_CLIENT_ID "$EDGIO_CLIENT_ID" |
|||
fi |
|||
|
|||
if [ -z "$EDGIO_CLIENT_SECRET" ]; then |
|||
_err "EDGIO_CLIENT_SECRET is not defined." |
|||
return 1 |
|||
else |
|||
_savedomainconf EDGIO_CLIENT_SECRET "$EDGIO_CLIENT_SECRET" |
|||
fi |
|||
|
|||
if [ -z "$EDGIO_ENVIRONMENT_ID" ]; then |
|||
_err "EDGIO_ENVIRONMENT_ID is not defined." |
|||
return 1 |
|||
else |
|||
_savedomainconf EDGIO_ENVIRONMENT_ID "$EDGIO_ENVIRONMENT_ID" |
|||
fi |
|||
|
|||
_info "Getting access token" |
|||
_data="client_id=$EDGIO_CLIENT_ID&client_secret=$EDGIO_CLIENT_SECRET&grant_type=client_credentials&scope=app.config" |
|||
_debug Get_access_token_data "$_data" |
|||
_response=$(_post "$_data" "https://id.edgio.app/connect/token" "" "POST" "application/x-www-form-urlencoded") |
|||
_debug Get_access_token_response "$_response" |
|||
_access_token=$(echo "$_response" | _json_decode | _egrep_o '"access_token":"[^"]*' | cut -d : -f 2 | tr -d '"') |
|||
_debug _access_token "$_access_token" |
|||
if [ -z "$_access_token" ]; then |
|||
_err "Error in getting access token" |
|||
return 1 |
|||
fi |
|||
|
|||
_info "Uploading certificate" |
|||
string_ccert=$(sed 's/$/\\n/' "$_ccert" | tr -d '\n') |
|||
string_cca=$(sed 's/$/\\n/' "$_cca" | tr -d '\n') |
|||
string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n') |
|||
|
|||
for ENVIRONMENT_ID in $EDGIO_ENVIRONMENT_ID; do |
|||
_data="{\"environment_id\":\"$ENVIRONMENT_ID\",\"primary_cert\":\"$string_ccert\",\"intermediate_cert\":\"$string_cca\",\"private_key\":\"$string_key\"}" |
|||
_debug Upload_certificate_data "$_data" |
|||
_H1="Authorization: Bearer $_access_token" |
|||
_response=$(_post "$_data" "https://edgioapis.com/config/v0.1/tls-certs" "" "POST" "application/json") |
|||
if _contains "$_response" "message"; then |
|||
_err "Error in deploying $_cdomain certificate to Edgio ENVIRONMENT_ID $ENVIRONMENT_ID." |
|||
_err "$_response" |
|||
return 1 |
|||
fi |
|||
_debug Upload_certificate_response "$_response" |
|||
_info "Domain $_cdomain certificate successfully deployed to Edgio ENVIRONMENT_ID $ENVIRONMENT_ID." |
|||
done |
|||
|
|||
return 0 |
|||
} |
|||
@ -0,0 +1,98 @@ |
|||
#!/usr/bin/env sh |
|||
|
|||
#Here is a script to deploy cert to a Kemp Loadmaster. |
|||
|
|||
#returns 0 means success, otherwise error. |
|||
|
|||
#DEPLOY_KEMP_TOKEN="token" |
|||
#DEPLOY_KEMP_URL="https://kemplm.example.com" |
|||
|
|||
######## Public functions ##################### |
|||
|
|||
#domain keyfile certfile cafile fullchain |
|||
kemplm_deploy() { |
|||
_domain="$1" |
|||
_key_file="$2" |
|||
_cert_file="$3" |
|||
_ca_file="$4" |
|||
_fullchain_file="$5" |
|||
|
|||
_debug _domain "$_domain" |
|||
_debug _key_file "$_key_file" |
|||
_debug _cert_file "$_cert_file" |
|||
_debug _ca_file "$_ca_file" |
|||
_debug _fullchain_file "$_fullchain_file" |
|||
|
|||
if ! _exists jq; then |
|||
_err "jq not found" |
|||
return 1 |
|||
fi |
|||
|
|||
# Rename wildcard certs, kemp accepts only alphanumeric names so we delete '*.' from filename |
|||
_kemp_domain=$(echo "${_domain}" | sed 's/\*\.//') |
|||
_debug _kemp_domain "$_kemp_domain" |
|||
|
|||
# Read config from saved values or env |
|||
_getdeployconf DEPLOY_KEMP_TOKEN |
|||
_getdeployconf DEPLOY_KEMP_URL |
|||
|
|||
_debug DEPLOY_KEMP_URL "$DEPLOY_KEMP_URL" |
|||
_secure_debug DEPLOY_KEMP_TOKEN "$DEPLOY_KEMP_TOKEN" |
|||
|
|||
if [ -z "$DEPLOY_KEMP_TOKEN" ]; then |
|||
_err "Kemp Loadmaster token is not found, please define DEPLOY_KEMP_TOKEN." |
|||
return 1 |
|||
fi |
|||
if [ -z "$DEPLOY_KEMP_URL" ]; then |
|||
_err "Kemp Loadmaster URL is not found, please define DEPLOY_KEMP_URL." |
|||
return 1 |
|||
fi |
|||
|
|||
# Save current values |
|||
_savedeployconf DEPLOY_KEMP_TOKEN "$DEPLOY_KEMP_TOKEN" |
|||
_savedeployconf DEPLOY_KEMP_URL "$DEPLOY_KEMP_URL" |
|||
|
|||
# Check if certificate is already installed |
|||
_info "Check if certificate is already present" |
|||
_list_request="{\"cmd\": \"listcert\", \"apikey\": \"${DEPLOY_KEMP_TOKEN}\"}" |
|||
_debug3 _list_request "${_list_request}" |
|||
_kemp_cert_count=$(HTTPS_INSECURE=1 _post "${_list_request}" "${DEPLOY_KEMP_URL}/accessv2" | jq -r '.cert[] | .name' | grep -c "${_kemp_domain}") |
|||
_debug2 _kemp_cert_count "${_kemp_cert_count}" |
|||
|
|||
_kemp_replace_cert=1 |
|||
if [ "${_kemp_cert_count}" -eq 0 ]; then |
|||
_kemp_replace_cert=0 |
|||
_info "Certificate does not exist on Kemp Loadmaster" |
|||
else |
|||
_info "Certificate already exists on Kemp Loadmaster" |
|||
fi |
|||
_debug _kemp_replace_cert "${_kemp_replace_cert}" |
|||
|
|||
# Upload new certificate to Kemp Loadmaster |
|||
_kemp_upload_cert=$(_mktemp) |
|||
cat "${_fullchain_file}" "${_key_file}" | base64 | tr -d '\n' >"${_kemp_upload_cert}" |
|||
|
|||
_info "Uploading certificate to Kemp Loadmaster" |
|||
_add_data=$(cat "${_kemp_upload_cert}") |
|||
_add_request="{\"cmd\": \"addcert\", \"apikey\": \"${DEPLOY_KEMP_TOKEN}\", \"replace\": ${_kemp_replace_cert}, \"cert\": \"${_kemp_domain}\", \"data\": \"${_add_data}\"}" |
|||
_debug3 _add_request "${_add_request}" |
|||
_kemp_post_result=$(HTTPS_INSECURE=1 _post "${_add_request}" "${DEPLOY_KEMP_URL}/accessv2") |
|||
_retval=$? |
|||
_debug2 _kemp_post_result "${_kemp_post_result}" |
|||
if [ "${_retval}" -eq 0 ]; then |
|||
_kemp_post_status=$(echo "${_kemp_post_result}" | jq -r '.status') |
|||
_kemp_post_message=$(echo "${_kemp_post_result}" | jq -r '.message') |
|||
if [ "${_kemp_post_status}" = "ok" ]; then |
|||
_info "Upload successful" |
|||
else |
|||
_err "Upload failed: ${_kemp_post_message}" |
|||
fi |
|||
else |
|||
_err "Upload failed" |
|||
_retval=1 |
|||
fi |
|||
|
|||
rm "${_kemp_upload_cert}" |
|||
|
|||
return $_retval |
|||
} |
|||
@ -0,0 +1,131 @@ |
|||
#!/usr/bin/env sh |
|||
|
|||
# Script to deploy certificate to KeyHelp |
|||
# This deployment required following variables |
|||
# export DEPLOY_KEYHELP_BASEURL="https://keyhelp.example.com" |
|||
# export DEPLOY_KEYHELP_USERNAME="Your KeyHelp Username" |
|||
# export DEPLOY_KEYHELP_PASSWORD="Your KeyHelp Password" |
|||
# export DEPLOY_KEYHELP_DOMAIN_ID="Depoly certificate to this Domain ID" |
|||
|
|||
# Open the 'Edit domain' page, and you will see id=xxx at the end of the URL. This is the Domain ID. |
|||
# https://DEPLOY_KEYHELP_BASEURL/index.php?page=domains&action=edit&id=xxx |
|||
|
|||
# If have more than one domain name |
|||
# export DEPLOY_KEYHELP_DOMAIN_ID="111 222 333" |
|||
|
|||
keyhelp_deploy() { |
|||
_cdomain="$1" |
|||
_ckey="$2" |
|||
_ccert="$3" |
|||
_cca="$4" |
|||
_cfullchain="$5" |
|||
|
|||
_debug _cdomain "$_cdomain" |
|||
_debug _ckey "$_ckey" |
|||
_debug _ccert "$_ccert" |
|||
_debug _cca "$_cca" |
|||
_debug _cfullchain "$_cfullchain" |
|||
|
|||
if [ -z "$DEPLOY_KEYHELP_BASEURL" ]; then |
|||
_err "DEPLOY_KEYHELP_BASEURL is not defined." |
|||
return 1 |
|||
else |
|||
_savedomainconf DEPLOY_KEYHELP_BASEURL "$DEPLOY_KEYHELP_BASEURL" |
|||
fi |
|||
|
|||
if [ -z "$DEPLOY_KEYHELP_USERNAME" ]; then |
|||
_err "DEPLOY_KEYHELP_USERNAME is not defined." |
|||
return 1 |
|||
else |
|||
_savedomainconf DEPLOY_KEYHELP_USERNAME "$DEPLOY_KEYHELP_USERNAME" |
|||
fi |
|||
|
|||
if [ -z "$DEPLOY_KEYHELP_PASSWORD" ]; then |
|||
_err "DEPLOY_KEYHELP_PASSWORD is not defined." |
|||
return 1 |
|||
else |
|||
_savedomainconf DEPLOY_KEYHELP_PASSWORD "$DEPLOY_KEYHELP_PASSWORD" |
|||
fi |
|||
|
|||
if [ -z "$DEPLOY_KEYHELP_DOMAIN_ID" ]; then |
|||
_err "DEPLOY_KEYHELP_DOMAIN_ID is not defined." |
|||
return 1 |
|||
else |
|||
_savedomainconf DEPLOY_KEYHELP_DOMAIN_ID "$DEPLOY_KEYHELP_DOMAIN_ID" |
|||
fi |
|||
|
|||
# Optional DEPLOY_KEYHELP_ENFORCE_HTTPS |
|||
_getdeployconf DEPLOY_KEYHELP_ENFORCE_HTTPS |
|||
# set default values for DEPLOY_KEYHELP_ENFORCE_HTTPS |
|||
[ -n "${DEPLOY_KEYHELP_ENFORCE_HTTPS}" ] || DEPLOY_KEYHELP_ENFORCE_HTTPS="1" |
|||
|
|||
_info "Logging in to keyhelp panel" |
|||
username_encoded="$(printf "%s" "${DEPLOY_KEYHELP_USERNAME}" | _url_encode)" |
|||
password_encoded="$(printf "%s" "${DEPLOY_KEYHELP_PASSWORD}" | _url_encode)" |
|||
_H1="Content-Type: application/x-www-form-urlencoded" |
|||
_response=$(_get "$DEPLOY_KEYHELP_BASEURL/index.php?submit=1&username=$username_encoded&password=$password_encoded" "TRUE") |
|||
_cookie="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _head_n 1 | cut -d " " -f 2)" |
|||
|
|||
# If cookies is not empty then logon successful |
|||
if [ -z "$_cookie" ]; then |
|||
_err "Fail to get cookie." |
|||
return 1 |
|||
fi |
|||
_debug "cookie" "$_cookie" |
|||
|
|||
_info "Uploading certificate" |
|||
_date=$(date +"%Y%m%d") |
|||
encoded_key="$(_url_encode <"$_ckey")" |
|||
encoded_ccert="$(_url_encode <"$_ccert")" |
|||
encoded_cca="$(_url_encode <"$_cca")" |
|||
certificate_name="$_cdomain-$_date" |
|||
|
|||
_request_body="submit=1&certificate_name=$certificate_name&add_type=upload&text_private_key=$encoded_key&text_certificate=$encoded_ccert&text_ca_certificate=$encoded_cca" |
|||
_H1="Cookie: $_cookie" |
|||
_response=$(_post "$_request_body" "$DEPLOY_KEYHELP_BASEURL/index.php?page=ssl_certificates&action=add" "" "POST") |
|||
_message=$(echo "$_response" | grep -A 2 'message-body' | sed -n '/<div class="message-body ">/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//') |
|||
_info "_message" "$_message" |
|||
if [ -z "$_message" ]; then |
|||
_err "Fail to upload certificate." |
|||
return 1 |
|||
fi |
|||
|
|||
for DOMAIN_ID in $DEPLOY_KEYHELP_DOMAIN_ID; do |
|||
_info "Apply certificate to domain id $DOMAIN_ID" |
|||
_response=$(_get "$DEPLOY_KEYHELP_BASEURL/index.php?page=domains&action=edit&id=$DOMAIN_ID") |
|||
cert_value=$(echo "$_response" | grep "$certificate_name" | sed -n 's/.*value="\([^"]*\).*/\1/p') |
|||
target_type=$(echo "$_response" | grep 'target_type' | grep 'checked' | sed -n 's/.*value="\([^"]*\).*/\1/p') |
|||
if [ "$target_type" = "directory" ]; then |
|||
path=$(echo "$_response" | awk '/name="path"/{getline; print}' | sed -n 's/.*value="\([^"]*\).*/\1/p') |
|||
fi |
|||
echo "$_response" | grep "is_prefer_https" | grep "checked" >/dev/null |
|||
if [ $? -eq 0 ]; then |
|||
is_prefer_https=1 |
|||
else |
|||
is_prefer_https=0 |
|||
fi |
|||
echo "$_response" | grep "hsts_enabled" | grep "checked" >/dev/null |
|||
if [ $? -eq 0 ]; then |
|||
hsts_enabled=1 |
|||
else |
|||
hsts_enabled=0 |
|||
fi |
|||
_debug "cert_value" "$cert_value" |
|||
if [ -z "$cert_value" ]; then |
|||
_err "Fail to get certificate id." |
|||
return 1 |
|||
fi |
|||
|
|||
_request_body="submit=1&id=$DOMAIN_ID&target_type=$target_type&path=$path&is_prefer_https=$is_prefer_https&hsts_enabled=$hsts_enabled&certificate_type=custom&certificate_id=$cert_value&enforce_https=$DEPLOY_KEYHELP_ENFORCE_HTTPS" |
|||
_response=$(_post "$_request_body" "$DEPLOY_KEYHELP_BASEURL/index.php?page=domains&action=edit" "" "POST") |
|||
_message=$(echo "$_response" | grep -A 2 'message-body' | sed -n '/<div class="message-body ">/,/<\/div>/{//!p;}' | sed 's/<[^>]*>//g' | sed 's/^ *//;s/ *$//') |
|||
_info "_message" "$_message" |
|||
if [ -z "$_message" ]; then |
|||
_err "Fail to apply certificate." |
|||
return 1 |
|||
fi |
|||
done |
|||
|
|||
_info "Domain $_cdomain certificate successfully deployed to KeyHelp Domain ID $DEPLOY_KEYHELP_DOMAIN_ID." |
|||
return 0 |
|||
} |
|||
@ -0,0 +1,86 @@ |
|||
#!/usr/bin/env sh |
|||
|
|||
keyhelp_api_deploy() { |
|||
_cdomain="$1" |
|||
_ckey="$2" |
|||
_ccert="$3" |
|||
_cca="$4" |
|||
|
|||
_debug _cdomain "$_cdomain" |
|||
_debug _ckey "$_ckey" |
|||
_debug _ccert "$_ccert" |
|||
_debug _cca "$_cca" |
|||
|
|||
# Read config from saved values or env |
|||
_getdeployconf DEPLOY_KEYHELP_HOST |
|||
_getdeployconf DEPLOY_KEYHELP_API_KEY |
|||
|
|||
_debug DEPLOY_KEYHELP_HOST "$DEPLOY_KEYHELP_HOST" |
|||
_secure_debug DEPLOY_KEYHELP_API_KEY "$DEPLOY_KEYHELP_API_KEY" |
|||
|
|||
if [ -z "$DEPLOY_KEYHELP_HOST" ]; then |
|||
_err "KeyHelp host not found, please define DEPLOY_KEYHELP_HOST." |
|||
return 1 |
|||
fi |
|||
if [ -z "$DEPLOY_KEYHELP_API_KEY" ]; then |
|||
_err "KeyHelp api key not found, please define DEPLOY_KEYHELP_API_KEY." |
|||
return 1 |
|||
fi |
|||
|
|||
# Save current values |
|||
_savedeployconf DEPLOY_KEYHELP_HOST "$DEPLOY_KEYHELP_HOST" |
|||
_savedeployconf DEPLOY_KEYHELP_API_KEY "$DEPLOY_KEYHELP_API_KEY" |
|||
|
|||
_request_key="$(tr '\n' ':' <"$_ckey" | sed 's/:/\\n/g')" |
|||
_request_cert="$(tr '\n' ':' <"$_ccert" | sed 's/:/\\n/g')" |
|||
_request_ca="$(tr '\n' ':' <"$_cca" | sed 's/:/\\n/g')" |
|||
|
|||
_request_body="{ |
|||
\"name\": \"$_cdomain\", |
|||
\"components\": { |
|||
\"private_key\": \"$_request_key\", |
|||
\"certificate\": \"$_request_cert\", |
|||
\"ca_certificate\": \"$_request_ca\" |
|||
} |
|||
}" |
|||
|
|||
_hosts="$(echo "$DEPLOY_KEYHELP_HOST" | tr "," " ")" |
|||
_keys="$(echo "$DEPLOY_KEYHELP_API_KEY" | tr "," " ")" |
|||
_i=1 |
|||
|
|||
for _host in $_hosts; do |
|||
_key="$(_getfield "$_keys" "$_i" " ")" |
|||
_i="$(_math "$_i" + 1)" |
|||
|
|||
export _H1="X-API-Key: $_key" |
|||
|
|||
_put_url="$_host/api/v2/certificates/name/$_cdomain" |
|||
if _post "$_request_body" "$_put_url" "" "PUT" "application/json" >/dev/null; then |
|||
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" |
|||
else |
|||
_err "Cannot make PUT request to $_put_url" |
|||
return 1 |
|||
fi |
|||
|
|||
if [ "$_code" = "404" ]; then |
|||
_info "$_cdomain not found, creating new entry at $_host" |
|||
|
|||
_post_url="$_host/api/v2/certificates" |
|||
if _post "$_request_body" "$_post_url" "" "POST" "application/json" >/dev/null; then |
|||
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" |
|||
else |
|||
_err "Cannot make POST request to $_post_url" |
|||
return 1 |
|||
fi |
|||
fi |
|||
|
|||
if _startswith "$_code" "2"; then |
|||
_info "$_cdomain set at $_host" |
|||
else |
|||
_err "HTTP status code is $_code" |
|||
return 1 |
|||
fi |
|||
done |
|||
|
|||
return 0 |
|||
} |
|||
@ -0,0 +1,276 @@ |
|||
#!/usr/bin/env sh |
|||
|
|||
################################################################################ |
|||
# ACME.sh 3rd party deploy plugin for multiple (same) services |
|||
################################################################################ |
|||
# Authors: tomo2403 (creator), https://github.com/tomo2403 |
|||
# Updated: 2025-03-01 |
|||
# Issues: https://github.com/acmesh-official/acme.sh/issues and mention @tomo2403 |
|||
################################################################################ |
|||
# Usage (shown values are the examples): |
|||
# 1. Set optional environment variables |
|||
# - export MULTIDEPLOY_FILENAME="multideploy.yaml" - "multideploy.yml" will be automatically used if not set" |
|||
# |
|||
# 2. Run command: |
|||
# acme.sh --deploy --deploy-hook multideploy -d example.com |
|||
################################################################################ |
|||
# Dependencies: |
|||
# - yq |
|||
################################################################################ |
|||
# Return value: |
|||
# 0 means success, otherwise error. |
|||
################################################################################ |
|||
|
|||
MULTIDEPLOY_VERSION="1.0" |
|||
|
|||
# Description: This function handles the deployment of certificates to multiple services. |
|||
# It processes the provided certificate files and deploys them according to the |
|||
# configuration specified in the multideploy file. |
|||
# |
|||
# Parameters: |
|||
# _cdomain - The domain name for which the certificate is issued. |
|||
# _ckey - The private key file for the certificate. |
|||
# _ccert - The certificate file. |
|||
# _cca - The CA (Certificate Authority) file. |
|||
# _cfullchain - The full chain certificate file. |
|||
# _cpfx - The PFX (Personal Information Exchange) file. |
|||
multideploy_deploy() { |
|||
_cdomain="$1" |
|||
_ckey="$2" |
|||
_ccert="$3" |
|||
_cca="$4" |
|||
_cfullchain="$5" |
|||
_cpfx="$6" |
|||
|
|||
_debug _cdomain "$_cdomain" |
|||
_debug _ckey "$_ckey" |
|||
_debug _ccert "$_ccert" |
|||
_debug _cca "$_cca" |
|||
_debug _cfullchain "$_cfullchain" |
|||
_debug _cpfx "$_cpfx" |
|||
|
|||
MULTIDEPLOY_FILENAME="${MULTIDEPLOY_FILENAME:-$(_getdeployconf MULTIDEPLOY_FILENAME)}" |
|||
if [ -z "$MULTIDEPLOY_FILENAME" ]; then |
|||
MULTIDEPLOY_FILENAME="multideploy.yml" |
|||
_info "MULTIDEPLOY_FILENAME is not set, so I will use 'multideploy.yml'." |
|||
else |
|||
_savedeployconf "MULTIDEPLOY_FILENAME" "$MULTIDEPLOY_FILENAME" |
|||
_debug2 "MULTIDEPLOY_FILENAME" "$MULTIDEPLOY_FILENAME" |
|||
fi |
|||
|
|||
if ! file=$(_preprocess_deployfile "$MULTIDEPLOY_FILENAME"); then |
|||
_err "Failed to preprocess deploy file." |
|||
return 1 |
|||
fi |
|||
_debug3 "File" "$file" |
|||
|
|||
# Deploy to services |
|||
_deploy_services "$file" |
|||
_exitCode="$?" |
|||
|
|||
return "$_exitCode" |
|||
} |
|||
|
|||
# Description: |
|||
# This function preprocesses the deploy file by checking if 'yq' is installed, |
|||
# verifying the existence of the deploy file, and ensuring only one deploy file is present. |
|||
# Arguments: |
|||
# $@ - Posible deploy file names. |
|||
# Usage: |
|||
# _preprocess_deployfile "<deploy_file1>" "<deploy_file2>?" |
|||
_preprocess_deployfile() { |
|||
# Check if yq is installed |
|||
if ! command -v yq >/dev/null 2>&1; then |
|||
_err "yq is not installed! Please install yq and try again." |
|||
return 1 |
|||
fi |
|||
_debug3 "yq is installed." |
|||
|
|||
# Check if deploy file exists |
|||
for file in "$@"; do |
|||
_debug3 "Checking file" "$DOMAIN_PATH/$file" |
|||
if [ -f "$DOMAIN_PATH/$file" ]; then |
|||
_debug3 "File found" |
|||
if [ -n "$found_file" ]; then |
|||
_err "Multiple deploy files found. Please keep only one deploy file." |
|||
return 1 |
|||
fi |
|||
found_file="$file" |
|||
else |
|||
_debug3 "File not found" |
|||
fi |
|||
done |
|||
|
|||
if [ -z "$found_file" ]; then |
|||
_err "Deploy file not found. Go to https://github.com/acmesh-official/acme.sh/wiki/deployhooks#36-deploying-to-multiple-services-with-the-same-hooks to see how to create one." |
|||
return 1 |
|||
fi |
|||
if ! _check_deployfile "$DOMAIN_PATH/$found_file"; then |
|||
_err "Deploy file is not valid: $DOMAIN_PATH/$found_file" |
|||
return 1 |
|||
fi |
|||
|
|||
echo "$DOMAIN_PATH/$found_file" |
|||
} |
|||
|
|||
# Description: |
|||
# This function checks the deploy file for version compatibility and the existence of the specified configuration and services. |
|||
# Arguments: |
|||
# $1 - The path to the deploy configuration file. |
|||
# $2 - The name of the deploy configuration to use. |
|||
# Usage: |
|||
# _check_deployfile "<deploy_file_path>" |
|||
_check_deployfile() { |
|||
_deploy_file="$1" |
|||
_debug2 "check: Deploy file" "$_deploy_file" |
|||
|
|||
# Check version |
|||
_deploy_file_version=$(yq -r '.version' "$_deploy_file") |
|||
if [ "$MULTIDEPLOY_VERSION" != "$_deploy_file_version" ]; then |
|||
_err "As of $PROJECT_NAME $VER, the deploy file needs version $MULTIDEPLOY_VERSION! Your current deploy file is of version $_deploy_file_version." |
|||
return 1 |
|||
fi |
|||
_debug2 "check: Deploy file version is compatible: $_deploy_file_version" |
|||
|
|||
# Extract all services from config |
|||
_services=$(yq -r '.services[].name' "$_deploy_file") |
|||
|
|||
if [ -z "$_services" ]; then |
|||
_err "Config does not have any services to deploy to." |
|||
return 1 |
|||
fi |
|||
_debug2 "check: Config has services." |
|||
echo "$_services" | while read -r _service; do |
|||
_debug3 " - $_service" |
|||
done |
|||
|
|||
# Check if extracted services exist in services list |
|||
echo "$_services" | while read -r _service; do |
|||
_debug2 "check: Checking service: $_service" |
|||
# Check if service exists |
|||
_service_config=$(yq -r ".services[] | select(.name == \"$_service\")" "$_deploy_file") |
|||
if [ -z "$_service_config" ] || [ "$_service_config" = "null" ]; then |
|||
_err "Service '$_service' not found." |
|||
return 1 |
|||
fi |
|||
|
|||
_service_hook=$(echo "$_service_config" | yq -r ".hook" -) |
|||
if [ -z "$_service_hook" ] || [ "$_service_hook" = "null" ]; then |
|||
_err "Service '$_service' does not have a hook." |
|||
return 1 |
|||
fi |
|||
|
|||
_service_environment=$(echo "$_service_config" | yq -r ".environment" -) |
|||
if [ -z "$_service_environment" ] || [ "$_service_environment" = "null" ]; then |
|||
_err "Service '$_service' does not have an environment." |
|||
return 1 |
|||
fi |
|||
done |
|||
} |
|||
|
|||
# Description: This function takes a list of environment variables in YAML format, |
|||
# parses them, and exports each key-value pair as environment variables. |
|||
# Arguments: |
|||
# $1 - A string containing the list of environment variables in YAML format. |
|||
# Usage: |
|||
# _export_envs "$env_list" |
|||
_export_envs() { |
|||
_env_list="$1" |
|||
|
|||
_secure_debug3 "Exporting envs" "$_env_list" |
|||
|
|||
echo "$_env_list" | yq -r 'to_entries | .[] | .key + "=" + .value' | while IFS='=' read -r _key _value; do |
|||
# Using eval to expand nested variables in the configuration file |
|||
_value=$(eval 'echo "'"$_value"'"') |
|||
_savedeployconf "$_key" "$_value" |
|||
_secure_debug3 "Saved $_key" "$_value" |
|||
done |
|||
} |
|||
|
|||
# Description: |
|||
# This function takes a YAML formatted string of environment variables, parses it, |
|||
# and clears each environment variable. It logs the process of clearing each variable. |
|||
# |
|||
# Note: Environment variables for a hook may be optional and differ between |
|||
# services using the same hook. |
|||
# If one service sets optional environment variables and another does not, the |
|||
# variables may persist and affect subsequent deployments. |
|||
# Clearing these variables after each service ensures that only the |
|||
# environment variables explicitly specified for each service in the deploy |
|||
# file are used. |
|||
# Arguments: |
|||
# $1 - A YAML formatted string containing environment variable key-value pairs. |
|||
# Usage: |
|||
# _clear_envs "<yaml_string>" |
|||
_clear_envs() { |
|||
_env_list="$1" |
|||
|
|||
_secure_debug3 "Clearing envs" "$_env_list" |
|||
env_pairs=$(echo "$_env_list" | yq -r 'to_entries | .[] | .key + "=" + .value') |
|||
|
|||
echo "$env_pairs" | while IFS='=' read -r _key _value; do |
|||
_debug3 "Deleting key" "$_key" |
|||
_cleardomainconf "SAVED_$_key" |
|||
unset -v "$_key" |
|||
done |
|||
} |
|||
|
|||
# Description: |
|||
# This function deploys services listed in the deploy configuration file. |
|||
# Arguments: |
|||
# $1 - The path to the deploy configuration file. |
|||
# $2 - The list of services to deploy. |
|||
# Usage: |
|||
# _deploy_services "<deploy_file_path>" "<services_list>" |
|||
_deploy_services() { |
|||
_deploy_file="$1" |
|||
_debug3 "Deploy file" "$_deploy_file" |
|||
|
|||
_tempfile=$(mktemp) |
|||
trap 'rm -f $_tempfile' EXIT |
|||
|
|||
yq -r '.services[].name' "$_deploy_file" >"$_tempfile" |
|||
_debug3 "Services" "$(cat "$_tempfile")" |
|||
|
|||
_failedServices="" |
|||
_failedCount=0 |
|||
while read -r _service <&3; do |
|||
_debug2 "Service" "$_service" |
|||
_hook=$(yq -r ".services[] | select(.name == \"$_service\").hook" "$_deploy_file") |
|||
_envs=$(yq -r ".services[] | select(.name == \"$_service\").environment" "$_deploy_file") |
|||
|
|||
_export_envs "$_envs" |
|||
if ! _deploy_service "$_service" "$_hook"; then |
|||
_failedServices="$_service, $_failedServices" |
|||
_failedCount=$((_failedCount + 1)) |
|||
fi |
|||
_clear_envs "$_envs" |
|||
done 3<"$_tempfile" |
|||
|
|||
_debug3 "Failed services" "$_failedServices" |
|||
_debug2 "Failed count" "$_failedCount" |
|||
if [ -n "$_failedServices" ]; then |
|||
_info "$(__red "Deployment failed") for services: $_failedServices" |
|||
else |
|||
_debug "All services deployed successfully." |
|||
fi |
|||
|
|||
return "$_failedCount" |
|||
} |
|||
|
|||
# Description: Deploys a service using the specified hook. |
|||
# Arguments: |
|||
# $1 - The name of the service to deploy. |
|||
# $2 - The hook to use for deployment. |
|||
# Usage: |
|||
# _deploy_service <service_name> <hook> |
|||
_deploy_service() { |
|||
_name="$1" |
|||
_hook="$2" |
|||
|
|||
_debug2 "SERVICE" "$_name" |
|||
_debug2 "HOOK" "$_hook" |
|||
|
|||
_info "$(__green "Deploying") to '$_name' using '$_hook'" |
|||
_deploy "$_cdomain" "$_hook" |
|||
} |
|||
@ -0,0 +1,69 @@ |
|||
#!/usr/bin/env sh |
|||
|
|||
# Script to deploy certificate to Netlify |
|||
# https://docs.netlify.com/api/get-started/#authentication |
|||
# https://open-api.netlify.com/#tag/sniCertificate |
|||
|
|||
# This deployment required following variables |
|||
# export Netlify_ACCESS_TOKEN="Your Netlify Access Token" |
|||
# export Netlify_SITE_ID="Your Netlify Site ID" |
|||
|
|||
# If have more than one SITE ID |
|||
# export Netlify_SITE_ID="SITE_ID_1 SITE_ID_2" |
|||
|
|||
# returns 0 means success, otherwise error. |
|||
|
|||
######## Public functions ##################### |
|||
|
|||
#domain keyfile certfile cafile fullchain |
|||
netlify_deploy() { |
|||
_cdomain="$1" |
|||
_ckey="$2" |
|||
_ccert="$3" |
|||
_cca="$4" |
|||
_cfullchain="$5" |
|||
|
|||
_debug _cdomain "$_cdomain" |
|||
_debug _ckey "$_ckey" |
|||
_debug _ccert "$_ccert" |
|||
_debug _cca "$_cca" |
|||
_debug _cfullchain "$_cfullchain" |
|||
|
|||
if [ -z "$Netlify_ACCESS_TOKEN" ]; then |
|||
_err "Netlify_ACCESS_TOKEN is not defined." |
|||
return 1 |
|||
else |
|||
_savedomainconf Netlify_ACCESS_TOKEN "$Netlify_ACCESS_TOKEN" |
|||
fi |
|||
if [ -z "$Netlify_SITE_ID" ]; then |
|||
_err "Netlify_SITE_ID is not defined." |
|||
return 1 |
|||
else |
|||
_savedomainconf Netlify_SITE_ID "$Netlify_SITE_ID" |
|||
fi |
|||
|
|||
_info "Deploying certificate to Netlify..." |
|||
|
|||
## upload certificate |
|||
string_ccert=$(sed 's/$/\\n/' "$_ccert" | tr -d '\n') |
|||
string_cca=$(sed 's/$/\\n/' "$_cca" | tr -d '\n') |
|||
string_key=$(sed 's/$/\\n/' "$_ckey" | tr -d '\n') |
|||
|
|||
for SITE_ID in $Netlify_SITE_ID; do |
|||
_request_body="{\"certificate\":\"$string_ccert\",\"key\":\"$string_key\",\"ca_certificates\":\"$string_cca\"}" |
|||
_debug _request_body "$_request_body" |
|||
_debug Netlify_ACCESS_TOKEN "$Netlify_ACCESS_TOKEN" |
|||
export _H1="Authorization: Bearer $Netlify_ACCESS_TOKEN" |
|||
_response=$(_post "$_request_body" "https://api.netlify.com/api/v1/sites/$SITE_ID/ssl" "" "POST" "application/json") |
|||
|
|||
if _contains "$_response" "\"error\""; then |
|||
_err "Error in deploying $_cdomain certificate to Netlify SITE_ID $SITE_ID." |
|||
_err "$_response" |
|||
return 1 |
|||
fi |
|||
_debug response "$_response" |
|||
_info "Domain $_cdomain certificate successfully deployed to Netlify SITE_ID $SITE_ID." |
|||
done |
|||
|
|||
return 0 |
|||
} |
|||
@ -0,0 +1,130 @@ |
|||
#!/usr/bin/env sh |
|||
|
|||
# Deploy certificates to a proxmox backup server using the API. |
|||
# |
|||
# Environment variables that can be set are: |
|||
# `DEPLOY_PROXMOXBS_SERVER`: The hostname of the proxmox backup server. Defaults to |
|||
# _cdomain. |
|||
# `DEPLOY_PROXMOXBS_SERVER_PORT`: The port number the management interface is on. |
|||
# Defaults to 8007. |
|||
# `DEPLOY_PROXMOXBS_USER`: The user we'll connect as. Defaults to root. |
|||
# `DEPLOY_PROXMOXBS_USER_REALM`: The authentication realm the user authenticates |
|||
# with. Defaults to pam. |
|||
# `DEPLOY_PROXMOXBS_API_TOKEN_NAME`: The name of the API token created for the |
|||
# user account. Defaults to acme. |
|||
# `DEPLOY_PROXMOXBS_API_TOKEN_KEY`: The API token. Required. |
|||
|
|||
proxmoxbs_deploy() { |
|||
_cdomain="$1" |
|||
_ckey="$2" |
|||
_ccert="$3" |
|||
_cca="$4" |
|||
_cfullchain="$5" |
|||
|
|||
_debug _cdomain "$_cdomain" |
|||
_debug2 _ckey "$_ckey" |
|||
_debug _ccert "$_ccert" |
|||
_debug _cca "$_cca" |
|||
_debug _cfullchain "$_cfullchain" |
|||
|
|||
# "Sane" defaults. |
|||
_getdeployconf DEPLOY_PROXMOXBS_SERVER |
|||
if [ -z "$DEPLOY_PROXMOXBS_SERVER" ]; then |
|||
_target_hostname="$_cdomain" |
|||
else |
|||
_target_hostname="$DEPLOY_PROXMOXBS_SERVER" |
|||
_savedeployconf DEPLOY_PROXMOXBS_SERVER "$DEPLOY_PROXMOXBS_SERVER" |
|||
fi |
|||
_debug2 DEPLOY_PROXMOXBS_SERVER "$_target_hostname" |
|||
|
|||
_getdeployconf DEPLOY_PROXMOXBS_SERVER_PORT |
|||
if [ -z "$DEPLOY_PROXMOXBS_SERVER_PORT" ]; then |
|||
_target_port="8007" |
|||
else |
|||
_target_port="$DEPLOY_PROXMOXBS_SERVER_PORT" |
|||
_savedeployconf DEPLOY_PROXMOXBS_SERVER_PORT "$DEPLOY_PROXMOXBS_SERVER_PORT" |
|||
fi |
|||
_debug2 DEPLOY_PROXMOXBS_SERVER_PORT "$_target_port" |
|||
|
|||
# Complete URL. |
|||
_target_url="https://${_target_hostname}:${_target_port}/api2/json/nodes/localhost/certificates/custom" |
|||
_debug TARGET_URL "$_target_url" |
|||
|
|||
# More "sane" defaults. |
|||
_getdeployconf DEPLOY_PROXMOXBS_USER |
|||
if [ -z "$DEPLOY_PROXMOXBS_USER" ]; then |
|||
_proxmoxbs_user="root" |
|||
else |
|||
_proxmoxbs_user="$DEPLOY_PROXMOXBS_USER" |
|||
_savedeployconf DEPLOY_PROXMOXBS_USER "$DEPLOY_PROXMOXBS_USER" |
|||
fi |
|||
_debug2 DEPLOY_PROXMOXBS_USER "$_proxmoxbs_user" |
|||
|
|||
_getdeployconf DEPLOY_PROXMOXBS_USER_REALM |
|||
if [ -z "$DEPLOY_PROXMOXBS_USER_REALM" ]; then |
|||
_proxmoxbs_user_realm="pam" |
|||
else |
|||
_proxmoxbs_user_realm="$DEPLOY_PROXMOXBS_USER_REALM" |
|||
_savedeployconf DEPLOY_PROXMOXBS_USER_REALM "$DEPLOY_PROXMOXBS_USER_REALM" |
|||
fi |
|||
_debug2 DEPLOY_PROXMOXBS_USER_REALM "$_proxmoxbs_user_realm" |
|||
|
|||
_getdeployconf DEPLOY_PROXMOXBS_API_TOKEN_NAME |
|||
if [ -z "$DEPLOY_PROXMOXBS_API_TOKEN_NAME" ]; then |
|||
_proxmoxbs_api_token_name="acme" |
|||
else |
|||
_proxmoxbs_api_token_name="$DEPLOY_PROXMOXBS_API_TOKEN_NAME" |
|||
_savedeployconf DEPLOY_PROXMOXBS_API_TOKEN_NAME "$DEPLOY_PROXMOXBS_API_TOKEN_NAME" |
|||
fi |
|||
_debug2 DEPLOY_PROXMOXBS_API_TOKEN_NAME "$_proxmoxbs_api_token_name" |
|||
|
|||
# This is required. |
|||
_getdeployconf DEPLOY_PROXMOXBS_API_TOKEN_KEY |
|||
if [ -z "$DEPLOY_PROXMOXBS_API_TOKEN_KEY" ]; then |
|||
_err "API key not provided." |
|||
return 1 |
|||
else |
|||
_proxmoxbs_api_token_key="$DEPLOY_PROXMOXBS_API_TOKEN_KEY" |
|||
_savedeployconf DEPLOY_PROXMOXBS_API_TOKEN_KEY "$DEPLOY_PROXMOXBS_API_TOKEN_KEY" |
|||
fi |
|||
_debug2 DEPLOY_PROXMOXBS_API_TOKEN_KEY "$_proxmoxbs_api_token_key" |
|||
|
|||
# PBS API Token header value. Used in "Authorization: PBSAPIToken". |
|||
_proxmoxbs_header_api_token="${_proxmoxbs_user}@${_proxmoxbs_user_realm}!${_proxmoxbs_api_token_name}:${_proxmoxbs_api_token_key}" |
|||
_debug2 "Auth Header" "$_proxmoxbs_header_api_token" |
|||
|
|||
# Ugly. I hate putting heredocs inside functions because heredocs don't |
|||
# account for whitespace correctly but it _does_ work and is several times |
|||
# cleaner than anything else I had here. |
|||
# |
|||
# This dumps the json payload to a variable that should be passable to the |
|||
# _psot function. |
|||
_json_payload=$( |
|||
cat <<HEREDOC |
|||
{ |
|||
"certificates": "$(tr '\n' ':' <"$_cfullchain" | sed 's/:/\\n/g')", |
|||
"key": "$(tr '\n' ':' <"$_ckey" | sed 's/:/\\n/g')", |
|||
"node":"localhost", |
|||
"restart":true, |
|||
"force":true |
|||
} |
|||
HEREDOC |
|||
) |
|||
_debug2 Payload "$_json_payload" |
|||
|
|||
_info "Push certificates to server" |
|||
export HTTPS_INSECURE=1 |
|||
export _H1="Authorization: PBSAPIToken=${_proxmoxbs_header_api_token}" |
|||
response=$(_post "$_json_payload" "$_target_url" "" POST "application/json") |
|||
_retval=$? |
|||
if [ "${_retval}" -eq 0 ]; then |
|||
_debug3 response "$response" |
|||
_info "Certificate successfully deployed" |
|||
return 0 |
|||
else |
|||
_err "Certificate deployment failed" |
|||
_debug "Response" "$response" |
|||
return 1 |
|||
fi |
|||
|
|||
} |
|||
@ -0,0 +1,343 @@ |
|||
#!/usr/bin/env sh |
|||
|
|||
# TrueNAS deploy script for SCALE/CORE using websocket |
|||
# It is recommend to use a wildcard certificate |
|||
# |
|||
# Websocket Documentation: https://www.truenas.com/docs/api/scale_websocket_api.html |
|||
# |
|||
# Tested with TrueNAS Scale - Electric Eel 24.10 |
|||
# Changes certificate in the following services: |
|||
# - Web UI |
|||
# - FTP |
|||
# - iX Apps |
|||
# |
|||
# The following environment variables must be set: |
|||
# ------------------------------------------------ |
|||
# |
|||
# # API KEY |
|||
# # Use the folowing URL to create a new API token: <TRUENAS_HOSTNAME OR IP>/ui/apikeys |
|||
# export DEPLOY_TRUENAS_APIKEY="<API_KEY_GENERATED_IN_THE_WEB_UI" |
|||
# |
|||
|
|||
### Private functions |
|||
|
|||
# Call websocket method |
|||
# Usage: |
|||
# _ws_response=$(_ws_call "math.dummycalc" "'{"x": 4, "y": 5}'") |
|||
# _info "$_ws_response" |
|||
# |
|||
# Output: |
|||
# {"z": 9} |
|||
# |
|||
# Arguments: |
|||
# $@ - midclt arguments for call |
|||
# |
|||
# Returns: |
|||
# JSON/JOBID |
|||
_ws_call() { |
|||
_debug "_ws_call arg1" "$1" |
|||
_debug "_ws_call arg2" "$2" |
|||
_debug "_ws_call arg3" "$3" |
|||
if [ $# -eq 3 ]; then |
|||
_ws_response=$(midclt --uri "$_ws_uri" -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2" "$3") |
|||
fi |
|||
if [ $# -eq 2 ]; then |
|||
_ws_response=$(midclt --uri "$_ws_uri" -K "$DEPLOY_TRUENAS_APIKEY" call "$1" "$2") |
|||
fi |
|||
if [ $# -eq 1 ]; then |
|||
_ws_response=$(midclt --uri "$_ws_uri" -K "$DEPLOY_TRUENAS_APIKEY" call "$1") |
|||
fi |
|||
_debug "_ws_response" "$_ws_response" |
|||
printf "%s" "$_ws_response" |
|||
return 0 |
|||
} |
|||
|
|||
# Upload certificate with webclient api |
|||
_ws_upload_cert() { |
|||
|
|||
/usr/bin/env python - <<EOF |
|||
|
|||
import sys |
|||
|
|||
from truenas_api_client import Client |
|||
with Client(uri="$_ws_uri") as c: |
|||
|
|||
### Login with API key |
|||
print("I:Trying to upload new certificate...") |
|||
ret = c.call("auth.login_with_api_key", "${DEPLOY_TRUENAS_APIKEY}") |
|||
if ret: |
|||
### upload certificate |
|||
with open('$1', 'r') as file: |
|||
fullchain = file.read() |
|||
with open('$2', 'r') as file: |
|||
privatekey = file.read() |
|||
ret = c.call("certificate.create", {"name": "$3", "create_type": "CERTIFICATE_CREATE_IMPORTED", "certificate": fullchain, "privatekey": privatekey}, job=True) |
|||
print("R:" + str(ret["id"])) |
|||
sys.exit(0) |
|||
else: |
|||
print("R:0") |
|||
print("E:_ws_upload_cert error!") |
|||
sys.exit(7) |
|||
|
|||
EOF |
|||
|
|||
return $? |
|||
|
|||
} |
|||
|
|||
# Check argument is a number |
|||
# Usage: |
|||
# |
|||
# Output: |
|||
# n/a |
|||
# |
|||
# Arguments: |
|||
# $1 - Anything |
|||
# |
|||
# Returns: |
|||
# 0: true |
|||
# 1: false |
|||
_ws_check_jobid() { |
|||
case "$1" in |
|||
[0-9]*) |
|||
return 0 |
|||
;; |
|||
esac |
|||
return 1 |
|||
} |
|||
|
|||
# Wait for job to finish and return result as JSON |
|||
# Usage: |
|||
# _ws_result=$(_ws_get_job_result "$_ws_jobid") |
|||
# _new_certid=$(printf "%s" "$_ws_result" | jq -r '."id"') |
|||
# |
|||
# Output: |
|||
# JSON result of the job |
|||
# |
|||
# Arguments: |
|||
# $1 - JobID |
|||
# |
|||
# Returns: |
|||
# n/a |
|||
_ws_get_job_result() { |
|||
while true; do |
|||
_sleep 2 |
|||
_ws_response=$(_ws_call "core.get_jobs" "[[\"id\", \"=\", $1]]") |
|||
if [ "$(printf "%s" "$_ws_response" | jq -r '.[]."state"')" != "RUNNING" ]; then |
|||
_ws_result="$(printf "%s" "$_ws_response" | jq '.[]."result"')" |
|||
_debug "_ws_result" "$_ws_result" |
|||
printf "%s" "$_ws_result" |
|||
_ws_error="$(printf "%s" "$_ws_response" | jq '.[]."error"')" |
|||
if [ "$_ws_error" != "null" ]; then |
|||
_err "Job $1 failed:" |
|||
_err "$_ws_error" |
|||
return 7 |
|||
fi |
|||
break |
|||
fi |
|||
done |
|||
return 0 |
|||
} |
|||
|
|||
######################## |
|||
### Public functions ### |
|||
######################## |
|||
|
|||
# truenas_ws_deploy |
|||
# |
|||
# Deploy new certificate to TrueNAS services |
|||
# |
|||
# Arguments |
|||
# 1: Domain |
|||
# 2: Key-File |
|||
# 3: Certificate-File |
|||
# 4: CA-File |
|||
# 5: FullChain-File |
|||
# Returns: |
|||
# 0: Success |
|||
# 1: Missing API Key |
|||
# 2: TrueNAS not ready |
|||
# 3: Not a JobID |
|||
# 4: FTP cert error |
|||
# 5: WebUI cert error |
|||
# 6: Job error |
|||
# 7: WS call error |
|||
# |
|||
truenas_ws_deploy() { |
|||
_domain="$1" |
|||
_file_key="$2" |
|||
_file_cert="$3" |
|||
_file_ca="$4" |
|||
_file_fullchain="$5" |
|||
_debug _domain "$_domain" |
|||
_debug _file_key "$_file_key" |
|||
_debug _file_cert "$_file_cert" |
|||
_debug _file_ca "$_file_ca" |
|||
_debug _file_fullchain "$_file_fullchain" |
|||
|
|||
########## Environment check |
|||
|
|||
_info "Checking environment variables..." |
|||
_getdeployconf DEPLOY_TRUENAS_APIKEY |
|||
_getdeployconf DEPLOY_TRUENAS_HOSTNAME |
|||
_getdeployconf DEPLOY_TRUENAS_PROTOCOL |
|||
# Check API Key |
|||
if [ -z "$DEPLOY_TRUENAS_APIKEY" ]; then |
|||
_err "TrueNAS API key not found, please set the DEPLOY_TRUENAS_APIKEY environment variable." |
|||
return 1 |
|||
fi |
|||
# Check Hostname, default to localhost if not set |
|||
if [ -z "$DEPLOY_TRUENAS_HOSTNAME" ]; then |
|||
_info "TrueNAS hostname not set. Using 'localhost'." |
|||
DEPLOY_TRUENAS_HOSTNAME="localhost" |
|||
fi |
|||
# Check protocol, default to ws if not set |
|||
if [ -z "$DEPLOY_TRUENAS_PROTOCOL" ]; then |
|||
_info "TrueNAS protocol not set. Using 'ws'." |
|||
DEPLOY_TRUENAS_PROTOCOL="ws" |
|||
fi |
|||
_ws_uri="$DEPLOY_TRUENAS_PROTOCOL://$DEPLOY_TRUENAS_HOSTNAME/websocket" |
|||
_debug2 DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME" |
|||
_debug2 DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL" |
|||
_debug _ws_uri "$_ws_uri" |
|||
_secure_debug2 DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY" |
|||
_info "Environment variables: OK" |
|||
|
|||
########## Health check |
|||
|
|||
_info "Checking TrueNAS health..." |
|||
_ws_response=$(_ws_call "system.ready" | tr '[:lower:]' '[:upper:]') |
|||
_ws_ret=$? |
|||
if [ $_ws_ret -gt 0 ]; then |
|||
_err "Error calling system.ready:" |
|||
_err "$_ws_response" |
|||
return $_ws_ret |
|||
fi |
|||
|
|||
if [ "$_ws_response" != "TRUE" ]; then |
|||
_err "TrueNAS is not ready." |
|||
_err "Please check environment variables DEPLOY_TRUENAS_APIKEY, DEPLOY_TRUENAS_HOSTNAME and DEPLOY_TRUENAS_PROTOCOL." |
|||
_err "Verify API key." |
|||
return 2 |
|||
fi |
|||
_savedeployconf DEPLOY_TRUENAS_APIKEY "$DEPLOY_TRUENAS_APIKEY" |
|||
_savedeployconf DEPLOY_TRUENAS_HOSTNAME "$DEPLOY_TRUENAS_HOSTNAME" |
|||
_savedeployconf DEPLOY_TRUENAS_PROTOCOL "$DEPLOY_TRUENAS_PROTOCOL" |
|||
_info "TrueNAS health: OK" |
|||
|
|||
########## System info |
|||
|
|||
_info "Gather system info..." |
|||
_ws_response=$(_ws_call "system.info") |
|||
_truenas_version=$(printf "%s" "$_ws_response" | jq -r '."version"') |
|||
_info "TrueNAS version: $_truenas_version" |
|||
|
|||
########## Gather current certificate |
|||
|
|||
_info "Gather current WebUI certificate..." |
|||
_ws_response="$(_ws_call "system.general.config")" |
|||
_ui_certificate_id=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."id"') |
|||
_ui_certificate_name=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."name"') |
|||
_info "Current WebUI certificate ID: $_ui_certificate_id" |
|||
_info "Current WebUI certificate name: $_ui_certificate_name" |
|||
|
|||
########## Upload new certificate |
|||
|
|||
_info "Upload new certificate..." |
|||
_certname="acme_$(_utc_date | tr -d '\-\:' | tr ' ' '_')" |
|||
_info "New WebUI certificate name: $_certname" |
|||
_debug _certname "$_certname" |
|||
_ws_out=$(_ws_upload_cert "$_file_fullchain" "$_file_key" "$_certname") |
|||
|
|||
echo "$_ws_out" | while IFS= read -r LINE; do |
|||
case "$LINE" in |
|||
I:*) |
|||
_info "${LINE#I:}" |
|||
;; |
|||
D:*) |
|||
_debug "${LINE#D:}" |
|||
;; |
|||
E*) |
|||
_err "${LINE#E:}" |
|||
;; |
|||
*) ;; |
|||
|
|||
esac |
|||
done |
|||
|
|||
_new_certid=$(echo "$_ws_out" | grep 'R:' | cut -d ':' -f 2) |
|||
|
|||
_info "New certificate ID: $_new_certid" |
|||
|
|||
########## FTP |
|||
|
|||
_info "Replace FTP certificate..." |
|||
_ws_response=$(_ws_call "ftp.update" "{\"ssltls_certificate\": $_new_certid}") |
|||
_ftp_certid=$(printf "%s" "$_ws_response" | jq -r '."ssltls_certificate"') |
|||
if [ "$_ftp_certid" != "$_new_certid" ]; then |
|||
_err "Cannot set FTP certificate." |
|||
_debug "_ws_response" "$_ws_response" |
|||
return 4 |
|||
fi |
|||
|
|||
########## ix Apps (SCALE only) |
|||
|
|||
_info "Replace app certificates..." |
|||
_ws_response=$(_ws_call "app.query") |
|||
for _app_name in $(printf "%s" "$_ws_response" | jq -r '.[]."name"'); do |
|||
_info "Checking app $_app_name..." |
|||
_ws_response=$(_ws_call "app.config" "$_app_name") |
|||
if [ "$(printf "%s" "$_ws_response" | jq -r '."network" | has("certificate_id")')" = "true" ]; then |
|||
_info "App has certificate option, setup new certificate..." |
|||
_info "App will be redeployed after updating the certificate." |
|||
_ws_jobid=$(_ws_call "app.update" "$_app_name" "{\"values\": {\"network\": {\"certificate_id\": $_new_certid}}}") |
|||
_debug "_ws_jobid" "$_ws_jobid" |
|||
if ! _ws_check_jobid "$_ws_jobid"; then |
|||
_err "No JobID returned from websocket method." |
|||
return 3 |
|||
fi |
|||
_ws_result=$(_ws_get_job_result "$_ws_jobid") |
|||
_ws_ret=$? |
|||
if [ $_ws_ret -gt 0 ]; then |
|||
return $_ws_ret |
|||
fi |
|||
_debug "_ws_result" "$_ws_result" |
|||
_info "App certificate replaced." |
|||
else |
|||
_info "App has no certificate option, skipping..." |
|||
fi |
|||
done |
|||
|
|||
########## WebUI |
|||
|
|||
_info "Replace WebUI certificate..." |
|||
_ws_response=$(_ws_call "system.general.update" "{\"ui_certificate\": $_new_certid}") |
|||
_changed_certid=$(printf "%s" "$_ws_response" | jq -r '."ui_certificate"."id"') |
|||
if [ "$_changed_certid" != "$_new_certid" ]; then |
|||
_err "WebUI certificate change error.." |
|||
return 5 |
|||
else |
|||
_info "WebUI certificate replaced." |
|||
fi |
|||
_info "Restarting WebUI..." |
|||
_ws_response=$(_ws_call "system.general.ui_restart") |
|||
_info "Waiting for UI restart..." |
|||
_sleep 15 |
|||
|
|||
########## Certificates |
|||
|
|||
_info "Deleting old certificate..." |
|||
_ws_jobid=$(_ws_call "certificate.delete" "$_ui_certificate_id") |
|||
if ! _ws_check_jobid "$_ws_jobid"; then |
|||
_err "No JobID returned from websocket method." |
|||
return 3 |
|||
fi |
|||
_ws_result=$(_ws_get_job_result "$_ws_jobid") |
|||
_ws_ret=$? |
|||
if [ $_ws_ret -gt 0 ]; then |
|||
return $_ws_ret |
|||
fi |
|||
|
|||
_info "Have a nice day...bye!" |
|||
|
|||
} |
|||
@ -0,0 +1,500 @@ |
|||
#!/usr/bin/env sh |
|||
|
|||
# Deploy certificates to Zyxel GS1900 series switches |
|||
# |
|||
# This script uses the https web administration interface in order |
|||
# to upload updated certificates to Zyxel GS1900 series switches. |
|||
# Only a few models have been tested but untested switches from the |
|||
# same model line may work as well. If you test and confirm a switch |
|||
# as working please submit a pull request updating this compatibility |
|||
# list! |
|||
# |
|||
# Known Issues: |
|||
# 1. This is a consumer grade switch and is a bit underpowered |
|||
# the longer the RSA key size the slower your switch web UI |
|||
# will be. RSA 2048 will work, RSA 4096 will work but you may |
|||
# experience performance problems. |
|||
# 2. You must use RSA certificates. The switch will reject EC-256 |
|||
# and EC-384 certificates in firmware 2.80 |
|||
# See: https://community.zyxel.com/en/discussion/21506/bug-cannot-import-ssl-cert-on-gs1900-8-and-gs1900-24e-firmware-v2-80/ |
|||
# |
|||
# Current GS1900 Switch Compatibility: |
|||
# GS1900-8 - Working as of firmware V2.80 |
|||
# GS1900-8HP - Untested |
|||
# GS1900-10HP - Untested |
|||
# GS1900-16 - Untested |
|||
# GS1900-24 - Untested |
|||
# GS1900-24E - Working as of firmware V2.80 |
|||
# GS1900-24EP - Untested |
|||
# GS1900-24HP - Untested |
|||
# GS1900-48 - Untested |
|||
# GS1900-48HP - Untested |
|||
# |
|||
# Prerequisite Setup Steps: |
|||
# 1. Install at least firmware V2.80 on your switch |
|||
# 2. Enable HTTPS web management on your switch |
|||
# |
|||
# Usage: |
|||
# 1. Ensure the switch has firmware V2.80 or later. |
|||
# 2. Ensure the switch has HTTPS management enabled. |
|||
# 3. Set the appropriate environment variables for your environment. |
|||
# |
|||
# DEPLOY_ZYXEL_SWITCH - The switch hostname. (Default: _cdomain) |
|||
# DEPLOY_ZYXEL_SWITCH_USER - The webadmin user. (Default: admin) |
|||
# DEPLOY_ZYXEL_SWITCH_PASSWORD - The webadmin password for the switch. |
|||
# DEPLOY_ZYXEL_SWITCH_REBOOT - If "1" reboot after update. (Default: "0") |
|||
# |
|||
# 4. Run the deployment plugin: |
|||
# acme.sh --deploy --deploy-hook zyxel_gs1900 -d example.com |
|||
# |
|||
# returns 0 means success, otherwise error. |
|||
|
|||
#domain keyfile certfile cafile fullchain |
|||
zyxel_gs1900_deploy() { |
|||
_zyxel_gs1900_minimum_firmware_version="v2.80" |
|||
|
|||
_cdomain="$1" |
|||
_ckey="$2" |
|||
_ccert="$3" |
|||
_cca="$4" |
|||
_cfullchain="$5" |
|||
|
|||
_debug _cdomain "$_cdomain" |
|||
_debug2 _ckey "$_ckey" |
|||
_debug _ccert "$_ccert" |
|||
_debug _cca "$_cca" |
|||
_debug _cfullchain "$_cfullchain" |
|||
|
|||
_getdeployconf DEPLOY_ZYXEL_SWITCH |
|||
_getdeployconf DEPLOY_ZYXEL_SWITCH_USER |
|||
_getdeployconf DEPLOY_ZYXEL_SWITCH_PASSWORD |
|||
_getdeployconf DEPLOY_ZYXEL_SWITCH_REBOOT |
|||
|
|||
if [ -z "$DEPLOY_ZYXEL_SWITCH" ]; then |
|||
DEPLOY_ZYXEL_SWITCH="$_cdomain" |
|||
fi |
|||
|
|||
if [ -z "$DEPLOY_ZYXEL_SWITCH_USER" ]; then |
|||
DEPLOY_ZYXEL_SWITCH_USER="admin" |
|||
fi |
|||
|
|||
if [ -z "$DEPLOY_ZYXEL_SWITCH_PASSWORD" ]; then |
|||
DEPLOY_ZYXEL_SWITCH_PASSWORD="1234" |
|||
fi |
|||
|
|||
if [ -z "$DEPLOY_ZYXEL_SWITCH_REBOOT" ]; then |
|||
DEPLOY_ZYXEL_SWITCH_REBOOT="0" |
|||
fi |
|||
|
|||
_savedeployconf DEPLOY_ZYXEL_SWITCH "$DEPLOY_ZYXEL_SWITCH" |
|||
_savedeployconf DEPLOY_ZYXEL_SWITCH_USER "$DEPLOY_ZYXEL_SWITCH_USER" |
|||
_savedeployconf DEPLOY_ZYXEL_SWITCH_PASSWORD "$DEPLOY_ZYXEL_SWITCH_PASSWORD" |
|||
_savedeployconf DEPLOY_ZYXEL_SWITCH_REBOOT "$DEPLOY_ZYXEL_SWITCH_REBOOT" |
|||
|
|||
_debug DEPLOY_ZYXEL_SWITCH "$DEPLOY_ZYXEL_SWITCH" |
|||
_debug DEPLOY_ZYXEL_SWITCH_USER "$DEPLOY_ZYXEL_SWITCH_USER" |
|||
_secure_debug DEPLOY_ZYXEL_SWITCH_PASSWORD "$DEPLOY_ZYXEL_SWITCH_PASSWORD" |
|||
_debug DEPLOY_ZYXEL_SWITCH_REBOOT "$DEPLOY_ZYXEL_SWITCH_REBOOT" |
|||
|
|||
_zyxel_switch_base_uri="https://${DEPLOY_ZYXEL_SWITCH}" |
|||
|
|||
_info "Beginning to deploy to a Zyxel GS1900 series switch at ${_zyxel_switch_base_uri}." |
|||
_zyxel_gs1900_deployment_precheck || return $? |
|||
|
|||
_zyxel_gs1900_should_update |
|||
if [ "$?" != "0" ]; then |
|||
_info "The switch already has our certificate installed. No update required." |
|||
return 0 |
|||
else |
|||
_info "The switch does not yet have our certificate installed." |
|||
fi |
|||
|
|||
_info "Logging into the switch web interface." |
|||
_zyxel_gs1900_login || return $? |
|||
|
|||
_info "Validating the switch is compatible with this deployment process." |
|||
_zyxel_gs1900_validate_device_compatibility || return $? |
|||
|
|||
_info "Uploading the certificate." |
|||
_zyxel_gs1900_upload_certificate || return $? |
|||
|
|||
if [ "$DEPLOY_ZYXEL_SWITCH_REBOOT" = "1" ]; then |
|||
_info "Rebooting the switch." |
|||
_zyxel_gs1900_trigger_reboot || return $? |
|||
fi |
|||
|
|||
return 0 |
|||
} |
|||
|
|||
_zyxel_gs1900_deployment_precheck() { |
|||
# Initialize the keylength if it isn't already |
|||
if [ -z "$Le_Keylength" ]; then |
|||
Le_Keylength="" |
|||
fi |
|||
|
|||
if _isEccKey "$Le_Keylength"; then |
|||
_info "Warning: Zyxel GS1900 switches are not currently known to work with ECC keys!" |
|||
_info "You can continue, but your switch may reject your key." |
|||
elif [ -n "$Le_Keylength" ] && [ "$Le_Keylength" -gt "2048" ]; then |
|||
_info "Warning: Your RSA key length is greater than 2048!" |
|||
_info "You can continue, but you may experience performance issues in the web administration interface." |
|||
fi |
|||
|
|||
# Check the server for some common failure modes prior to authentication and certificate upload in order to avoid |
|||
# sending a certificate when we may not want to. |
|||
test_login_response=$(_post "username=test&password=test&login=true;" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=0.html" '' "POST" "application/x-www-form-urlencoded" 2>&1) |
|||
test_login_page_exitcode="$?" |
|||
_debug3 "Test Login Response: ${test_login_response}" |
|||
if [ "$test_login_page_exitcode" -ne "0" ]; then |
|||
if { [ "${ACME_USE_WGET:-0}" = "0" ] && [ "$test_login_page_exitcode" = "60" ]; } || { [ "${ACME_USE_WGET:-0}" = "1" ] && [ "$test_login_page_exitcode" = "5" ]; }; then |
|||
_err "The SSL certificate at $_zyxel_switch_base_uri could not be validated." |
|||
_err "Please double check your hostname, port, and that you are actually connecting to your switch." |
|||
_err "If the problem persists then please ensure that the certificate is not self-signed, has not" |
|||
_err "expired, and matches the switch hostname. If you expect validation to fail then you can disable" |
|||
_err "certificate validation by running with --insecure." |
|||
return 1 |
|||
elif [ "${ACME_USE_WGET:-0}" = "0" ] && [ "$test_login_page_exitcode" = "56" ]; then |
|||
_debug3 "Intentionally ignore curl exit code 56 in our precheck" |
|||
else |
|||
_err "Failed to submit the initial login attempt to $_zyxel_switch_base_uri." |
|||
return 1 |
|||
fi |
|||
fi |
|||
} |
|||
|
|||
_zyxel_gs1900_login() { |
|||
# Login to the switch and set the appropriate auth cookie in _H1 |
|||
username_encoded=$(printf "%s" "$DEPLOY_ZYXEL_SWITCH_USER" | _url_encode) |
|||
password_encoded=$(_zyxel_gs1900_password_obfuscate "$DEPLOY_ZYXEL_SWITCH_PASSWORD" | _url_encode) |
|||
|
|||
login_response=$(_post "username=${username_encoded}&password=${password_encoded}&login=true;" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=0.html" '' "POST" "application/x-www-form-urlencoded" | tr -d '\n') |
|||
auth_response=$(_post "authId=${login_response}&login_chk=true" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=0.html" '' "POST" "application/x-www-form-urlencoded" | tr -d '\n') |
|||
if [ "$auth_response" != "OK" ]; then |
|||
_err "Login failed due to invalid credentials." |
|||
_err "Please double check the configured username and password and try again." |
|||
return 1 |
|||
fi |
|||
|
|||
sessionid=$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'HTTPS_XSSID=[^;]*;' | tr -d ';') |
|||
_secure_debug2 "sessionid" "$sessionid" |
|||
|
|||
export _H1="Cookie: $sessionid" |
|||
_secure_debug2 "_H1" "$_H1" |
|||
|
|||
return 0 |
|||
} |
|||
|
|||
_zyxel_gs1900_validate_device_compatibility() { |
|||
# Check the switches model and firmware version and throw errors |
|||
# if this script isn't compatible. |
|||
device_info_html=$(_get "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=12" | tr -d '\n') |
|||
|
|||
model_name=$(_zyxel_gs1900_get_model "$device_info_html") |
|||
_debug2 "model_name" "$model_name" |
|||
if [ -z "$model_name" ]; then |
|||
_err "Could not find the switch model name." |
|||
_err "Please re-run with --debug and report a bug." |
|||
return $? |
|||
fi |
|||
|
|||
if ! expr "$model_name" : "GS1900-" >/dev/null; then |
|||
_err "Switch is an unsupported model: $model_name" |
|||
return 1 |
|||
fi |
|||
|
|||
firmware_version=$(_zyxel_gs1900_get_firmware_version "$device_info_html") |
|||
_debug2 "firmware_version" "$firmware_version" |
|||
if [ -z "$firmware_version" ]; then |
|||
_err "Could not find the switch firmware version." |
|||
_err "Please re-run with --debug and report a bug." |
|||
return $? |
|||
fi |
|||
|
|||
_debug2 "_zyxel_gs1900_minimum_firmware_version" "$_zyxel_gs1900_minimum_firmware_version" |
|||
minimum_major_version=$(_zyxel_gs1900_parse_major_version "$_zyxel_gs1900_minimum_firmware_version") |
|||
_debug2 "minimum_major_version" "$minimum_major_version" |
|||
minimum_minor_version=$(_zyxel_gs1900_parse_minor_version "$_zyxel_gs1900_minimum_firmware_version") |
|||
_debug2 "minimum_minor_version" "$minimum_minor_version" |
|||
|
|||
_debug2 "firmware_version" "$firmware_version" |
|||
firmware_major_version=$(_zyxel_gs1900_parse_major_version "$firmware_version") |
|||
_debug2 "firmware_major_version" "$firmware_major_version" |
|||
firmware_minor_version=$(_zyxel_gs1900_parse_minor_version "$firmware_version") |
|||
_debug2 "firmware_minor_version" "$firmware_minor_version" |
|||
|
|||
_ret=0 |
|||
if [ "$firmware_major_version" -lt "$minimum_major_version" ]; then |
|||
_ret=1 |
|||
elif [ "$firmware_major_version" -eq "$minimum_major_version" ] && [ "$firmware_minor_version" -lt "$minimum_minor_version" ]; then |
|||
_ret=1 |
|||
fi |
|||
|
|||
if [ "$_ret" != "0" ]; then |
|||
_err "Unsupported firmware version $firmware_version. Please upgrade to at least version $_zyxel_gs1900_minimum_firmware_version." |
|||
fi |
|||
|
|||
return $? |
|||
} |
|||
|
|||
_zyxel_gs1900_should_update() { |
|||
# Get the remote certificate serial number |
|||
_remote_cert=$(${ACME_OPENSSL_BIN:-openssl} s_client -showcerts -connect "${DEPLOY_ZYXEL_SWITCH}:443" 2>/dev/null </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p') |
|||
_debug3 "_remote_cert" "$_remote_cert" |
|||
|
|||
_remote_cert_serial=$(printf "%s" "${_remote_cert}" | ${ACME_OPENSSL_BIN:-openssl} x509 -noout -serial) |
|||
_debug2 "_remote_cert_serial" "$_remote_cert_serial" |
|||
|
|||
# Get our certificate serial number |
|||
_our_cert_serial=$(${ACME_OPENSSL_BIN:-openssl} x509 -noout -serial <"${_ccert}") |
|||
_debug2 "_our_cert_serial" "$_our_cert_serial" |
|||
|
|||
[ "${_remote_cert_serial}" != "${_our_cert_serial}" ] |
|||
} |
|||
|
|||
_zyxel_gs1900_upload_certificate() { |
|||
# Generate a PKCS12 certificate with a temporary password since the web interface |
|||
# requires a password be present. Then upload that certificate. |
|||
temp_cert_password=$(head /dev/urandom | tr -dc 'A-Za-z0-9' | head -c 64) |
|||
_secure_debug2 "temp_cert_password" "$temp_cert_password" |
|||
|
|||
temp_pkcs12="$(_mktemp)" |
|||
_debug2 "temp_pkcs12" "$temp_pkcs12" |
|||
_toPkcs "$temp_pkcs12" "$_ckey" "$_ccert" "$_cca" "$temp_cert_password" |
|||
if [ "$?" != "0" ]; then |
|||
_err "Failed to generate a pkcs12 certificate." |
|||
_err "Please re-run with --debug and report a bug." |
|||
|
|||
# ensure the temporary certificate file is cleaned up |
|||
[ -f "${temp_pkcs12}" ] && rm -f "${temp_pkcs12}" |
|||
|
|||
return $? |
|||
fi |
|||
|
|||
# Load the upload page |
|||
upload_page_html=$(_get "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=5914" | tr -d '\n') |
|||
|
|||
# Get the first instance of XSSID from the upload page |
|||
form_xss_value=$(printf "%s" "$upload_page_html" | _egrep_o 'name="XSSID"\s*value="[^"]+"' | sed 's/^.*="\([^"]\{1,\}\)"$/\1/g' | head -n 1) |
|||
_secure_debug2 "form_xss_value" "$form_xss_value" |
|||
|
|||
_info "Generating the certificate upload request" |
|||
upload_post_request="$(_mktemp)" |
|||
upload_post_boundary="---------------------------$(date +%Y%m%d%H%M%S)" |
|||
|
|||
{ |
|||
printf -- "--%s\r\n" "${upload_post_boundary}" |
|||
printf "Content-Disposition: form-data; name=\"XSSID\"\r\n\r\n%s\r\n" "${form_xss_value}" |
|||
printf -- "--%s\r\n" "${upload_post_boundary}" |
|||
printf "Content-Disposition: form-data; name=\"http_file\"; filename=\"temp_pkcs12.pfx\"\r\n" |
|||
printf "Content-Type: application/pkcs12\r\n\r\n" |
|||
cat "${temp_pkcs12}" |
|||
printf "\r\n" |
|||
printf -- "--%s\r\n" "${upload_post_boundary}" |
|||
printf "Content-Disposition: form-data; name=\"pwd\"\r\n\r\n%s\r\n" "${temp_cert_password}" |
|||
printf -- "--%s\r\n" "${upload_post_boundary}" |
|||
printf "Content-Disposition: form-data; name=\"cmd\"\r\n\r\n%s\r\n" "31" |
|||
printf -- "--%s\r\n" "${upload_post_boundary}" |
|||
printf "Content-Disposition: form-data; name=\"sysSubmit\"\r\n\r\n%s\r\n" "Import" |
|||
printf -- "--%s--\r\n" "${upload_post_boundary}" |
|||
} >"${upload_post_request}" |
|||
|
|||
_info "Upload certificate to the switch" |
|||
|
|||
# Unfortunately we cannot rely upon the switch response across switch models |
|||
# to return a consistent body return - so we cannot inspect the result of this |
|||
# upload to determine success. |
|||
upload_response=$(_zyxel_upload_pkcs12 "${upload_post_request}" "${upload_post_boundary}" 2>&1) |
|||
_debug3 "Upload response: ${upload_response}" |
|||
rm "${upload_post_request}" |
|||
|
|||
# Pause for a few seconds to give the switch a chance to process the certificate |
|||
# For some reason I've found this to be necessary on my GS1900-24E |
|||
_debug2 "Waiting 4 seconds for the switch to process the newly uploaded certificate." |
|||
sleep "4" |
|||
|
|||
# Check to see whether or not our update was successful |
|||
_ret=0 |
|||
_zyxel_gs1900_should_update |
|||
if [ "$?" != "0" ]; then |
|||
_info "The certificate was updated successfully" |
|||
else |
|||
_ret=1 |
|||
_err "The certificate upload does not appear to have worked." |
|||
_err "The remote certificate does not match the certificate we tried to upload." |
|||
_err "Please re-run with --debug 2 and review for unexpected errors. If none can be found please submit a bug." |
|||
fi |
|||
|
|||
# ensure the temporary files are cleaned up |
|||
[ -f "${temp_pkcs12}" ] && rm -f "${temp_pkcs12}" |
|||
|
|||
return $_ret |
|||
} |
|||
|
|||
# make the certificate upload request using either |
|||
# --data binary with @ for file access in CURL |
|||
# or using --post-file for wget to ensure we upload |
|||
# the pkcs12 without getting tripped up on null bytes |
|||
# |
|||
# Usage _zyxel_upload_pkcs12 [body file name] [post boundary marker] |
|||
_zyxel_upload_pkcs12() { |
|||
bodyfilename="$1" |
|||
multipartformmarker="$2" |
|||
_post_url="${_zyxel_switch_base_uri}/cgi-bin/httpuploadcert.cgi" |
|||
httpmethod="POST" |
|||
_postContentType="multipart/form-data; boundary=${multipartformmarker}" |
|||
|
|||
if [ -z "$httpmethod" ]; then |
|||
httpmethod="POST" |
|||
fi |
|||
_debug $httpmethod |
|||
_debug "_post_url" "$_post_url" |
|||
_debug2 "bodyfilename" "$bodyfilename" |
|||
_debug2 "_postContentType" "$_postContentType" |
|||
|
|||
_inithttp |
|||
|
|||
if [ "$_ACME_CURL" ] && [ "${ACME_USE_WGET:-0}" = "0" ]; then |
|||
_CURL="$_ACME_CURL" |
|||
if [ "$HTTPS_INSECURE" ]; then |
|||
_CURL="$_CURL --insecure " |
|||
fi |
|||
if [ "$httpmethod" = "HEAD" ]; then |
|||
_CURL="$_CURL -I " |
|||
fi |
|||
_debug "_CURL" "$_CURL" |
|||
|
|||
response="$($_CURL --user-agent "$USER_AGENT" -X $httpmethod -H "$_H1" -H "$_H2" -H "$_H3" -H "$_H4" -H "$_H5" --data-binary "@${bodyfilename}" "$_post_url")" |
|||
|
|||
_ret="$?" |
|||
if [ "$_ret" != "0" ]; then |
|||
_err "Please refer to https://curl.haxx.se/libcurl/c/libcurl-errors.html for error code: $_ret" |
|||
if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then |
|||
_err "Here is the curl dump log:" |
|||
_err "$(cat "$_CURL_DUMP")" |
|||
fi |
|||
fi |
|||
elif [ "$_ACME_WGET" ]; then |
|||
_WGET="$_ACME_WGET" |
|||
if [ "$HTTPS_INSECURE" ]; then |
|||
_WGET="$_WGET --no-check-certificate " |
|||
fi |
|||
_debug "_WGET" "$_WGET" |
|||
|
|||
response="$($_WGET -S -O - --user-agent="$USER_AGENT" --header "$_H5" --header "$_H4" --header "$_H3" --header "$_H2" --header "$_H1" --post-file="${bodyfilename}" "$_post_url" 2>"$HTTP_HEADER")" |
|||
|
|||
_ret="$?" |
|||
if [ "$_ret" = "8" ]; then |
|||
_ret=0 |
|||
_debug "wget returned 8 as the server returned a 'Bad Request' response. Let's process the response later." |
|||
fi |
|||
if [ "$_ret" != "0" ]; then |
|||
_err "Please refer to https://www.gnu.org/software/wget/manual/html_node/Exit-Status.html for error code: $_ret" |
|||
fi |
|||
if _contains "$_WGET" " -d "; then |
|||
# Demultiplex wget debug output |
|||
cat "$HTTP_HEADER" >&2 |
|||
_sed_i '/^[^ ][^ ]/d; /^ *$/d' "$HTTP_HEADER" |
|||
fi |
|||
# remove leading whitespaces from header to match curl format |
|||
_sed_i 's/^ //g' "$HTTP_HEADER" |
|||
else |
|||
_ret="$?" |
|||
_err "Neither curl nor wget have been found, cannot make $httpmethod request." |
|||
fi |
|||
_debug "_ret" "$_ret" |
|||
printf "%s" "$response" |
|||
return $_ret |
|||
} |
|||
|
|||
_zyxel_gs1900_trigger_reboot() { |
|||
# Trigger a reboot via the management reboot page in the web ui |
|||
reboot_page_html=$(_get "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi?cmd=5888" | tr -d '\n') |
|||
reboot_xss_value=$(printf "%s" "$reboot_page_html" | _egrep_o 'name="XSSID"\s*value="[^"]+"' | sed 's/^.*="\([^"]\{1,\}\)"$/\1/g') |
|||
_secure_debug2 "reboot_xss_value" "$reboot_xss_value" |
|||
|
|||
reboot_response_html=$(_post "XSSID=${reboot_xss_value}&cmd=5889&sysSubmit=Reboot" "${_zyxel_switch_base_uri}/cgi-bin/dispatcher.cgi" '' "POST" "application/x-www-form-urlencoded") |
|||
reboot_message=$(printf "%s" "$reboot_response_html" | tr -d '\t\r\n\v\f' | _egrep_o "Rebooting now...") |
|||
|
|||
if [ -z "$reboot_message" ]; then |
|||
_err "Failed to trigger switch reboot!" |
|||
return 1 |
|||
fi |
|||
|
|||
return 0 |
|||
} |
|||
|
|||
# password |
|||
_zyxel_gs1900_password_obfuscate() { |
|||
# Return the password obfuscated via the same method used by the |
|||
# switch's web UI login process |
|||
echo "$1" | awk '{ |
|||
encoded = ""; |
|||
password = $1; |
|||
allowed = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; |
|||
len = length($1); |
|||
pwi = length($1); |
|||
|
|||
for (i=1; i <= (321 - pwi); i++) |
|||
{ |
|||
if (0 == i % 5 && pwi > 0) |
|||
{ |
|||
encoded = (encoded)(substr(password, pwi--, 1)); |
|||
} |
|||
else if (i == 123) |
|||
{ |
|||
if (len < 10) |
|||
{ |
|||
encoded = (encoded)(0); |
|||
} |
|||
else |
|||
{ |
|||
encoded = (encoded)(int(len / 10)); |
|||
} |
|||
} |
|||
else if (i == 289) |
|||
{ |
|||
encoded = (encoded)(len % 10) |
|||
} |
|||
else |
|||
{ |
|||
encoded = (encoded)(substr(allowed, int(rand() * length(allowed)), 1)) |
|||
} |
|||
} |
|||
printf("%s", encoded); |
|||
}' |
|||
} |
|||
|
|||
# html label |
|||
_zyxel_html_table_lookup() { |
|||
# Look up a value in the html representing the status page of the switch |
|||
# when provided with the html of the page and the label (i.e. "Model Name:") |
|||
html="$1" |
|||
label=$(printf "%s" "$2" | tr -d ' ') |
|||
lookup_result=$(printf "%s" "$html" | tr -d "\t\r\n\v\f" | sed 's/<tr>/\n<tr>/g' | sed 's/<td[^>]*>/<td>/g' | tr -d ' ' | grep -i "$label" | sed "s/<tr><td>$label<\/td><td>\([^<]\{1,\}\)<\/td><\/tr>/\1/i") |
|||
printf "%s" "$lookup_result" |
|||
return 0 |
|||
} |
|||
|
|||
# html |
|||
_zyxel_gs1900_get_model() { |
|||
html="$1" |
|||
model_name=$(_zyxel_html_table_lookup "$html" "Model Name:") |
|||
printf "%s" "$model_name" |
|||
} |
|||
|
|||
# html |
|||
_zyxel_gs1900_get_firmware_version() { |
|||
html="$1" |
|||
firmware_version=$(_zyxel_html_table_lookup "$html" "Firmware Version:" | _egrep_o "V[^.]+.[^(]+") |
|||
printf "%s" "$firmware_version" |
|||
} |
|||
|
|||
# version_number |
|||
_zyxel_gs1900_parse_major_version() { |
|||
printf "%s" "$1" | sed 's/^V\([0-9]\{1,\}\).\{1,\}$/\1/gi' |
|||
} |
|||
|
|||
# version_number |
|||
_zyxel_gs1900_parse_minor_version() { |
|||
printf "%s" "$1" | sed 's/^.\{1,\}\.\([0-9]\{1,\}\)$/\1/gi' |
|||
} |
|||
@ -0,0 +1,281 @@ |
|||
#!/usr/bin/env sh |
|||
# shellcheck disable=SC2034 |
|||
dns_beget_info='Beget.com |
|||
Site: Beget.com |
|||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_beget |
|||
Options: |
|||
BEGET_User API user |
|||
BEGET_Password API password |
|||
Issues: github.com/acmesh-official/acme.sh/issues/6200 |
|||
Author: ARNik <arnik@arnik.ru> |
|||
' |
|||
|
|||
Beget_Api="https://api.beget.com/api" |
|||
|
|||
#################### Public functions #################### |
|||
|
|||
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" |
|||
# Used to add txt record |
|||
dns_beget_add() { |
|||
fulldomain=$1 |
|||
txtvalue=$2 |
|||
_debug "dns_beget_add() $fulldomain $txtvalue" |
|||
fulldomain=$(echo "$fulldomain" | _lower_case) |
|||
|
|||
Beget_Username="${Beget_Username:-$(_readaccountconf_mutable Beget_Username)}" |
|||
Beget_Password="${Beget_Password:-$(_readaccountconf_mutable Beget_Password)}" |
|||
|
|||
if [ -z "$Beget_Username" ] || [ -z "$Beget_Password" ]; then |
|||
Beget_Username="" |
|||
Beget_Password="" |
|||
_err "You must export variables: Beget_Username, and Beget_Password" |
|||
return 1 |
|||
fi |
|||
|
|||
#save the credentials to the account conf file. |
|||
_saveaccountconf_mutable Beget_Username "$Beget_Username" |
|||
_saveaccountconf_mutable Beget_Password "$Beget_Password" |
|||
|
|||
_info "Prepare subdomain." |
|||
if ! _prepare_subdomain "$fulldomain"; then |
|||
_err "Can't prepare subdomain." |
|||
return 1 |
|||
fi |
|||
|
|||
_info "Get domain records" |
|||
data="{\"fqdn\":\"$fulldomain\"}" |
|||
res=$(_api_call "$Beget_Api/dns/getData" "$data") |
|||
if ! _is_api_reply_ok "$res"; then |
|||
_err "Can't get domain records." |
|||
return 1 |
|||
fi |
|||
|
|||
_info "Add new TXT record" |
|||
data="{\"fqdn\":\"$fulldomain\",\"records\":{" |
|||
data=${data}$(_parce_records "$res" "A") |
|||
data=${data}$(_parce_records "$res" "AAAA") |
|||
data=${data}$(_parce_records "$res" "CAA") |
|||
data=${data}$(_parce_records "$res" "MX") |
|||
data=${data}$(_parce_records "$res" "SRV") |
|||
data=${data}$(_parce_records "$res" "TXT") |
|||
data=$(echo "$data" | sed 's/,$//') |
|||
data=${data}'}}' |
|||
|
|||
str=$(_txt_to_dns_json "$txtvalue") |
|||
data=$(_add_record "$data" "TXT" "$str") |
|||
|
|||
res=$(_api_call "$Beget_Api/dns/changeRecords" "$data") |
|||
if ! _is_api_reply_ok "$res"; then |
|||
_err "Can't change domain records." |
|||
return 1 |
|||
fi |
|||
|
|||
return 0 |
|||
} |
|||
|
|||
# Usage: fulldomain txtvalue |
|||
# Used to remove the txt record after validation |
|||
dns_beget_rm() { |
|||
fulldomain=$1 |
|||
txtvalue=$2 |
|||
_debug "dns_beget_rm() $fulldomain $txtvalue" |
|||
fulldomain=$(echo "$fulldomain" | _lower_case) |
|||
|
|||
Beget_Username="${Beget_Username:-$(_readaccountconf_mutable Beget_Username)}" |
|||
Beget_Password="${Beget_Password:-$(_readaccountconf_mutable Beget_Password)}" |
|||
|
|||
_info "Get current domain records" |
|||
data="{\"fqdn\":\"$fulldomain\"}" |
|||
res=$(_api_call "$Beget_Api/dns/getData" "$data") |
|||
if ! _is_api_reply_ok "$res"; then |
|||
_err "Can't get domain records." |
|||
return 1 |
|||
fi |
|||
|
|||
_info "Remove TXT record" |
|||
data="{\"fqdn\":\"$fulldomain\",\"records\":{" |
|||
data=${data}$(_parce_records "$res" "A") |
|||
data=${data}$(_parce_records "$res" "AAAA") |
|||
data=${data}$(_parce_records "$res" "CAA") |
|||
data=${data}$(_parce_records "$res" "MX") |
|||
data=${data}$(_parce_records "$res" "SRV") |
|||
data=${data}$(_parce_records "$res" "TXT") |
|||
data=$(echo "$data" | sed 's/,$//') |
|||
data=${data}'}}' |
|||
|
|||
str=$(_txt_to_dns_json "$txtvalue") |
|||
data=$(_rm_record "$data" "$str") |
|||
|
|||
res=$(_api_call "$Beget_Api/dns/changeRecords" "$data") |
|||
if ! _is_api_reply_ok "$res"; then |
|||
_err "Can't change domain records." |
|||
return 1 |
|||
fi |
|||
|
|||
return 0 |
|||
} |
|||
|
|||
#################### Private functions below #################### |
|||
|
|||
# Create subdomain if needed |
|||
# Usage: _prepare_subdomain [fulldomain] |
|||
_prepare_subdomain() { |
|||
fulldomain=$1 |
|||
|
|||
_info "Detect the root zone" |
|||
if ! _get_root "$fulldomain"; then |
|||
_err "invalid domain" |
|||
return 1 |
|||
fi |
|||
_debug _domain_id "$_domain_id" |
|||
_debug _sub_domain "$_sub_domain" |
|||
_debug _domain "$_domain" |
|||
|
|||
if [ -z "$_sub_domain" ]; then |
|||
_debug "$fulldomain is a root domain." |
|||
return 0 |
|||
fi |
|||
|
|||
_info "Get subdomain list" |
|||
res=$(_api_call "$Beget_Api/domain/getSubdomainList") |
|||
if ! _is_api_reply_ok "$res"; then |
|||
_err "Can't get subdomain list." |
|||
return 1 |
|||
fi |
|||
|
|||
if _contains "$res" "\"fqdn\":\"$fulldomain\""; then |
|||
_debug "Subdomain $fulldomain already exist." |
|||
return 0 |
|||
fi |
|||
|
|||
_info "Subdomain $fulldomain does not exist. Let's create one." |
|||
data="{\"subdomain\":\"$_sub_domain\",\"domain_id\":$_domain_id}" |
|||
res=$(_api_call "$Beget_Api/domain/addSubdomainVirtual" "$data") |
|||
if ! _is_api_reply_ok "$res"; then |
|||
_err "Can't create subdomain." |
|||
return 1 |
|||
fi |
|||
|
|||
_debug "Cleanup subdomen records" |
|||
data="{\"fqdn\":\"$fulldomain\",\"records\":{}}" |
|||
res=$(_api_call "$Beget_Api/dns/changeRecords" "$data") |
|||
if ! _is_api_reply_ok "$res"; then |
|||
_debug "Can't cleanup $fulldomain records." |
|||
fi |
|||
|
|||
data="{\"fqdn\":\"www.$fulldomain\",\"records\":{}}" |
|||
res=$(_api_call "$Beget_Api/dns/changeRecords" "$data") |
|||
if ! _is_api_reply_ok "$res"; then |
|||
_debug "Can't cleanup www.$fulldomain records." |
|||
fi |
|||
|
|||
return 0 |
|||
} |
|||
|
|||
# Usage: _get_root _acme-challenge.www.domain.com |
|||
#returns |
|||
# _sub_domain=_acme-challenge.www |
|||
# _domain=domain.com |
|||
# _domain_id=32436365 |
|||
_get_root() { |
|||
fulldomain=$1 |
|||
i=1 |
|||
p=1 |
|||
|
|||
_debug "Get domain list" |
|||
res=$(_api_call "$Beget_Api/domain/getList") |
|||
if ! _is_api_reply_ok "$res"; then |
|||
_err "Can't get domain list." |
|||
return 1 |
|||
fi |
|||
|
|||
while true; do |
|||
h=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-100) |
|||
_debug h "$h" |
|||
|
|||
if [ -z "$h" ]; then |
|||
return 1 |
|||
fi |
|||
|
|||
if _contains "$res" "$h"; then |
|||
_domain_id=$(echo "$res" | _egrep_o "\"id\":[0-9]*,\"fqdn\":\"$h\"" | cut -d , -f1 | cut -d : -f2) |
|||
if [ "$_domain_id" ]; then |
|||
if [ "$h" != "$fulldomain" ]; then |
|||
_sub_domain=$(echo "$fulldomain" | cut -d . -f 1-"$p") |
|||
else |
|||
_sub_domain="" |
|||
fi |
|||
_domain=$h |
|||
return 0 |
|||
fi |
|||
return 1 |
|||
fi |
|||
p="$i" |
|||
i=$(_math "$i" + 1) |
|||
done |
|||
return 1 |
|||
} |
|||
|
|||
# Parce DNS records from json string |
|||
# Usage: _parce_records [j_str] [record_name] |
|||
_parce_records() { |
|||
j_str=$1 |
|||
record_name=$2 |
|||
res="\"$record_name\":[" |
|||
res=${res}$(echo "$j_str" | _egrep_o "\"$record_name\":\[.*" | cut -d '[' -f2 | cut -d ']' -f1) |
|||
res=${res}"]," |
|||
echo "$res" |
|||
} |
|||
|
|||
# Usage: _add_record [data] [record_name] [record_data] |
|||
_add_record() { |
|||
data=$1 |
|||
record_name=$2 |
|||
record_data=$3 |
|||
echo "$data" | sed "s/\"$record_name\":\[/\"$record_name\":\[$record_data,/" | sed "s/,\]/\]/" |
|||
} |
|||
|
|||
# Usage: _rm_record [data] [record_data] |
|||
_rm_record() { |
|||
data=$1 |
|||
record_data=$2 |
|||
echo "$data" | sed "s/$record_data//g" | sed "s/,\+/,/g" | |
|||
sed "s/{,/{/g" | sed "s/,}/}/g" | |
|||
sed "s/\[,/\[/g" | sed "s/,\]/\]/g" |
|||
} |
|||
|
|||
_txt_to_dns_json() { |
|||
echo "{\"ttl\":600,\"txtdata\":\"$1\"}" |
|||
} |
|||
|
|||
# Usage: _api_call [api_url] [input_data] |
|||
_api_call() { |
|||
api_url="$1" |
|||
input_data="$2" |
|||
|
|||
_debug "_api_call $api_url" |
|||
_debug "Request: $input_data" |
|||
|
|||
# res=$(curl -s -L -D ./http.header \ |
|||
# "$api_url" \ |
|||
# --data-urlencode login=$Beget_Username \ |
|||
# --data-urlencode passwd=$Beget_Password \ |
|||
# --data-urlencode input_format=json \ |
|||
# --data-urlencode output_format=json \ |
|||
# --data-urlencode "input_data=$input_data") |
|||
|
|||
url="$api_url?login=$Beget_Username&passwd=$Beget_Password&input_format=json&output_format=json" |
|||
if [ -n "$input_data" ]; then |
|||
url=${url}"&input_data=" |
|||
url=${url}$(echo "$input_data" | _url_encode) |
|||
fi |
|||
res=$(_get "$url") |
|||
|
|||
_debug "Reply: $res" |
|||
echo "$res" |
|||
} |
|||
|
|||
# Usage: _is_api_reply_ok [api_reply] |
|||
_is_api_reply_ok() { |
|||
_contains "$1" '^{"status":"success","answer":{"status":"success","result":.*}}$' |
|||
} |
|||
@ -0,0 +1,163 @@ |
|||
#!/usr/bin/env sh |
|||
# shellcheck disable=SC2034 |
|||
dns_edgecenter_info='EdgeCenter.ru |
|||
Site: EdgeCenter.ru |
|||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_edgecenter |
|||
Options: |
|||
EDGECENTER_API_KEY API Key |
|||
Issues: github.com/acmesh-official/acme.sh/issues/6313 |
|||
Author: Konstantin Ruchev <konstantin.ruchev@edgecenter.ru> |
|||
' |
|||
|
|||
EDGECENTER_API="https://api.edgecenter.ru" |
|||
DOMAIN_TYPE= |
|||
DOMAIN_MASTER= |
|||
|
|||
######## Public functions ##################### |
|||
|
|||
#Usage: dns_edgecenter_add _acme-challenge.www.domain.com "TXT_RECORD_VALUE" |
|||
dns_edgecenter_add() { |
|||
fulldomain="$1" |
|||
txtvalue="$2" |
|||
|
|||
_info "Using EdgeCenter DNS API" |
|||
|
|||
if ! _dns_edgecenter_init_check; then |
|||
return 1 |
|||
fi |
|||
|
|||
_debug "Detecting root zone for $fulldomain" |
|||
if ! _get_root "$fulldomain"; then |
|||
return 1 |
|||
fi |
|||
|
|||
subdomain="${fulldomain%."$_zone"}" |
|||
subdomain=${subdomain%.} |
|||
|
|||
_debug "Zone: $_zone" |
|||
_debug "Subdomain: $subdomain" |
|||
_debug "TXT value: $txtvalue" |
|||
|
|||
payload='{"resource_records": [ { "content": ["'"$txtvalue"'"] } ], "ttl": 60 }' |
|||
_dns_edgecenter_http_api_call "post" "dns/v2/zones/$_zone/$subdomain.$_zone/txt" "$payload" |
|||
|
|||
if _contains "$response" '"error":"rrset is already exists"'; then |
|||
_debug "RRSet exists, merging values" |
|||
_dns_edgecenter_http_api_call "get" "dns/v2/zones/$_zone/$subdomain.$_zone/txt" |
|||
current="$response" |
|||
newlist="" |
|||
for v in $(echo "$current" | sed -n 's/.*"content":\["\([^"]*\)"\].*/\1/p'); do |
|||
newlist="$newlist {\"content\":[\"$v\"]}," |
|||
done |
|||
newlist="$newlist{\"content\":[\"$txtvalue\"]}" |
|||
putdata="{\"resource_records\":[${newlist}]} |
|||
" |
|||
_dns_edgecenter_http_api_call "put" "dns/v2/zones/$_zone/$subdomain.$_zone/txt" "$putdata" |
|||
_info "Updated existing RRSet with new TXT value." |
|||
return 0 |
|||
fi |
|||
|
|||
if _contains "$response" '"exception":'; then |
|||
_err "Record cannot be added." |
|||
return 1 |
|||
fi |
|||
|
|||
_info "TXT record added successfully." |
|||
return 0 |
|||
} |
|||
|
|||
#Usage: dns_edgecenter_rm _acme-challenge.www.domain.com "TXT_RECORD_VALUE" |
|||
dns_edgecenter_rm() { |
|||
fulldomain="$1" |
|||
txtvalue="$2" |
|||
|
|||
_info "Removing TXT record for $fulldomain" |
|||
|
|||
if ! _dns_edgecenter_init_check; then |
|||
return 1 |
|||
fi |
|||
|
|||
if ! _get_root "$fulldomain"; then |
|||
return 1 |
|||
fi |
|||
|
|||
subdomain="${fulldomain%."$_zone"}" |
|||
subdomain=${subdomain%.} |
|||
|
|||
_dns_edgecenter_http_api_call "delete" "dns/v2/zones/$_zone/$subdomain.$_zone/txt" |
|||
|
|||
if [ -z "$response" ]; then |
|||
_info "TXT record deleted successfully." |
|||
else |
|||
_info "TXT record may not have been deleted: $response" |
|||
fi |
|||
return 0 |
|||
} |
|||
|
|||
#################### Private functions below ################################## |
|||
|
|||
_dns_edgecenter_init_check() { |
|||
EDGECENTER_API_KEY="${EDGECENTER_API_KEY:-$(_readaccountconf_mutable EDGECENTER_API_KEY)}" |
|||
if [ -z "$EDGECENTER_API_KEY" ]; then |
|||
_err "EDGECENTER_API_KEY was not exported." |
|||
return 1 |
|||
fi |
|||
|
|||
_saveaccountconf_mutable EDGECENTER_API_KEY "$EDGECENTER_API_KEY" |
|||
export _H1="Authorization: APIKey $EDGECENTER_API_KEY" |
|||
|
|||
_dns_edgecenter_http_api_call "get" "dns/v2/clients/me/features" |
|||
if ! _contains "$response" '"id":'; then |
|||
_err "Invalid API key." |
|||
return 1 |
|||
fi |
|||
return 0 |
|||
} |
|||
|
|||
_get_root() { |
|||
domain="$1" |
|||
i=1 |
|||
while true; do |
|||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-) |
|||
if [ -z "$h" ]; then |
|||
return 1 |
|||
fi |
|||
_dns_edgecenter_http_api_call "get" "dns/v2/zones/$h" |
|||
if ! _contains "$response" 'zone is not found'; then |
|||
_zone="$h" |
|||
return 0 |
|||
fi |
|||
i=$((i + 1)) |
|||
done |
|||
return 1 |
|||
} |
|||
|
|||
_dns_edgecenter_http_api_call() { |
|||
mtd="$1" |
|||
endpoint="$2" |
|||
data="$3" |
|||
|
|||
export _H1="Authorization: APIKey $EDGECENTER_API_KEY" |
|||
|
|||
case "$mtd" in |
|||
get) |
|||
response="$(_get "$EDGECENTER_API/$endpoint")" |
|||
;; |
|||
post) |
|||
response="$(_post "$data" "$EDGECENTER_API/$endpoint")" |
|||
;; |
|||
delete) |
|||
response="$(_post "" "$EDGECENTER_API/$endpoint" "" "DELETE")" |
|||
;; |
|||
put) |
|||
response="$(_post "$data" "$EDGECENTER_API/$endpoint" "" "PUT")" |
|||
;; |
|||
*) |
|||
_err "Unknown HTTP method $mtd" |
|||
return 1 |
|||
;; |
|||
esac |
|||
|
|||
_debug "HTTP $mtd response: $response" |
|||
return 0 |
|||
} |
|||
@ -0,0 +1,139 @@ |
|||
#!/usr/bin/env sh |
|||
# shellcheck disable=SC2034 |
|||
dns_efficientip_info='efficientip.com |
|||
Site: https://efficientip.com/ |
|||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_efficientip |
|||
Options: |
|||
EfficientIP_Creds HTTP Basic Authentication credentials. E.g. "username:password" |
|||
EfficientIP_Server EfficientIP SOLIDserver Management IP address or FQDN. |
|||
EfficientIP_DNS_Name Name of the DNS smart or server hosting the zone. Optional. |
|||
EfficientIP_View Name of the DNS view hosting the zone. Optional. |
|||
OptionsAlt: |
|||
EfficientIP_Token_Key Alternative API token key, prefered over basic authentication. |
|||
EfficientIP_Token_Secret Alternative API token secret, required when using a token key. |
|||
EfficientIP_Server EfficientIP SOLIDserver Management IP address or FQDN. |
|||
EfficientIP_DNS_Name Name of the DNS smart or server hosting the zone. Optional. |
|||
EfficientIP_View Name of the DNS view hosting the zone. Optional. |
|||
Issues: github.com/acmesh-official/acme.sh/issues/6325 |
|||
Author: EfficientIP-Labs <contact@efficientip.com> |
|||
' |
|||
|
|||
dns_efficientip_add() { |
|||
fulldomain=$1 |
|||
txtvalue=$2 |
|||
|
|||
_info "Using EfficientIP API" |
|||
_debug fulldomain "$fulldomain" |
|||
_debug txtvalue "$txtvalue" |
|||
|
|||
if { [ -z "${EfficientIP_Creds}" ] && { [ -z "${EfficientIP_Token_Key}" ] || [ -z "${EfficientIP_Token_Secret}" ]; }; } || [ -z "${EfficientIP_Server}" ]; then |
|||
EfficientIP_Creds="" |
|||
EfficientIP_Token_Key="" |
|||
EfficientIP_Token_Secret="" |
|||
EfficientIP_Server="" |
|||
_err "You didn't specify any EfficientIP credentials or token or server (EfficientIP_Creds; EfficientIP_Token_Key; EfficientIP_Token_Secret; EfficientIP_Server)." |
|||
_err "Please set them via EXPORT EfficientIP_Creds=username:password or EXPORT EfficientIP_server=ip/hostname" |
|||
_err "or if you want to use Token instead EXPORT EfficientIP_Token_Key=yourkey" |
|||
_err "and EXPORT EfficientIP_Token_Secret=yoursecret" |
|||
_err "then try again." |
|||
return 1 |
|||
fi |
|||
|
|||
if [ -z "${EfficientIP_DNS_Name}" ]; then |
|||
EfficientIP_DNS_Name="" |
|||
fi |
|||
|
|||
EfficientIP_DNSNameEncoded=$(printf "%b" "${EfficientIP_DNS_Name}" | _url_encode) |
|||
|
|||
if [ -z "${EfficientIP_View}" ]; then |
|||
EfficientIP_View="" |
|||
fi |
|||
|
|||
EfficientIP_ViewEncoded=$(printf "%b" "${EfficientIP_View}" | _url_encode) |
|||
|
|||
_saveaccountconf EfficientIP_Creds "${EfficientIP_Creds}" |
|||
_saveaccountconf EfficientIP_Token_Key "${EfficientIP_Token_Key}" |
|||
_saveaccountconf EfficientIP_Token_Secret "${EfficientIP_Token_Secret}" |
|||
_saveaccountconf EfficientIP_Server "${EfficientIP_Server}" |
|||
_saveaccountconf EfficientIP_DNS_Name "${EfficientIP_DNS_Name}" |
|||
_saveaccountconf EfficientIP_View "${EfficientIP_View}" |
|||
|
|||
export _H1="Accept-Language:en-US" |
|||
baseurlnObject="https://${EfficientIP_Server}/rest/dns_rr_add?rr_type=TXT&rr_ttl=300&rr_name=${fulldomain}&rr_value1=${txtvalue}" |
|||
|
|||
if [ "${EfficientIP_DNSNameEncoded}" != "" ]; then |
|||
baseurlnObject="${baseurlnObject}&dns_name=${EfficientIP_DNSNameEncoded}" |
|||
fi |
|||
|
|||
if [ "${EfficientIP_ViewEncoded}" != "" ]; then |
|||
baseurlnObject="${baseurlnObject}&dnsview_name=${EfficientIP_ViewEncoded}" |
|||
fi |
|||
|
|||
if [ -z "${EfficientIP_Token_Secret}" ] || [ -z "${EfficientIP_Token_Key}" ]; then |
|||
EfficientIP_CredsEncoded=$(printf "%b" "${EfficientIP_Creds}" | _base64) |
|||
export _H2="Authorization: Basic ${EfficientIP_CredsEncoded}" |
|||
else |
|||
TS=$(date +%s) |
|||
Sig=$(printf "%b\n$TS\nPOST\n$baseurlnObject" "${EfficientIP_Token_Secret}" | _digest sha3-256 hex) |
|||
EfficientIP_CredsEncoded=$(printf "%b:%b" "${EfficientIP_Token_Key}" "$Sig") |
|||
export _H2="Authorization: SDS ${EfficientIP_CredsEncoded}" |
|||
export _H3="X-SDS-TS: ${TS}" |
|||
fi |
|||
|
|||
result="$(_post "" "${baseurlnObject}" "" "POST")" |
|||
|
|||
if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then |
|||
_info "DNS record successfully created" |
|||
return 0 |
|||
else |
|||
_err "Error creating DNS record" |
|||
_err "${result}" |
|||
return 1 |
|||
fi |
|||
} |
|||
|
|||
dns_efficientip_rm() { |
|||
fulldomain=$1 |
|||
txtvalue=$2 |
|||
|
|||
_info "Using EfficientIP API" |
|||
_debug fulldomain "${fulldomain}" |
|||
_debug txtvalue "${txtvalue}" |
|||
|
|||
EfficientIP_ViewEncoded=$(printf "%b" "${EfficientIP_View}" | _url_encode) |
|||
EfficientIP_DNSNameEncoded=$(printf "%b" "${EfficientIP_DNS_Name}" | _url_encode) |
|||
EfficientIP_CredsEncoded=$(printf "%b" "${EfficientIP_Creds}" | _base64) |
|||
|
|||
export _H1="Accept-Language:en-US" |
|||
|
|||
baseurlnObject="https://${EfficientIP_Server}/rest/dns_rr_delete?rr_type=TXT&rr_name=$fulldomain&rr_value1=$txtvalue" |
|||
if [ "${EfficientIP_DNSNameEncoded}" != "" ]; then |
|||
baseurlnObject="${baseurlnObject}&dns_name=${EfficientIP_DNSNameEncoded}" |
|||
fi |
|||
|
|||
if [ "${EfficientIP_ViewEncoded}" != "" ]; then |
|||
baseurlnObject="${baseurlnObject}&dnsview_name=${EfficientIP_ViewEncoded}" |
|||
fi |
|||
|
|||
if [ -z "$EfficientIP_Token_Secret" ] || [ -z "$EfficientIP_Token_Key" ]; then |
|||
EfficientIP_CredsEncoded=$(printf "%b" "${EfficientIP_Creds}" | _base64) |
|||
export _H2="Authorization: Basic $EfficientIP_CredsEncoded" |
|||
else |
|||
TS=$(date +%s) |
|||
Sig=$(printf "%b\n$TS\nDELETE\n${baseurlnObject}" "${EfficientIP_Token_Secret}" | _digest sha3-256 hex) |
|||
EfficientIP_CredsEncoded=$(printf "%b:%b" "${EfficientIP_Token_Key}" "$Sig") |
|||
export _H2="Authorization: SDS ${EfficientIP_CredsEncoded}" |
|||
export _H3="X-SDS-TS: $TS" |
|||
fi |
|||
|
|||
result="$(_post "" "${baseurlnObject}" "" "DELETE")" |
|||
|
|||
if [ "$(echo "${result}" | _egrep_o "ret_oid")" ]; then |
|||
_info "DNS Record successfully deleted" |
|||
return 0 |
|||
else |
|||
_err "Error deleting DNS record" |
|||
_err "${result}" |
|||
return 1 |
|||
fi |
|||
} |
|||
@ -0,0 +1,105 @@ |
|||
#!/usr/bin/env sh |
|||
# shellcheck disable=SC2034 |
|||
dns_freemyip_info='FreeMyIP.com |
|||
Site: FreeMyIP.com |
|||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_freemyip |
|||
Options: |
|||
FREEMYIP_Token API Token |
|||
Issues: github.com/acmesh-official/acme.sh/issues/6247 |
|||
Author: Recolic Keghart <root@recolic.net>, @Giova96 |
|||
' |
|||
|
|||
FREEMYIP_DNS_API="https://freemyip.com/update?" |
|||
|
|||
################ Public functions ################ |
|||
|
|||
#Usage: dns_freemyip_add fulldomain txtvalue |
|||
dns_freemyip_add() { |
|||
fulldomain="$1" |
|||
txtvalue="$2" |
|||
|
|||
_info "Add TXT record $txtvalue for $fulldomain using freemyip.com api" |
|||
|
|||
FREEMYIP_Token="${FREEMYIP_Token:-$(_readaccountconf_mutable FREEMYIP_Token)}" |
|||
if [ -z "$FREEMYIP_Token" ]; then |
|||
FREEMYIP_Token="" |
|||
_err "You don't specify FREEMYIP_Token yet." |
|||
_err "Please specify your token and try again." |
|||
return 1 |
|||
fi |
|||
|
|||
#save the credentials to the account conf file. |
|||
_saveaccountconf_mutable FREEMYIP_Token "$FREEMYIP_Token" |
|||
|
|||
if _is_root_domain_published "$fulldomain"; then |
|||
_err "freemyip API don't allow you to set multiple TXT record for the same subdomain!" |
|||
_err "You must apply certificate for only one domain at a time!" |
|||
_err "====" |
|||
_err "For example, aaa.yourdomain.freemyip.com and bbb.yourdomain.freemyip.com and yourdomain.freemyip.com ALWAYS share the same TXT record. They will overwrite each other if you apply multiple domain at the same time." |
|||
_debug "If you are testing this workflow in github pipeline or acmetest, please set TEST_DNS_NO_SUBDOMAIN=1 and TEST_DNS_NO_WILDCARD=1" |
|||
return 1 |
|||
fi |
|||
|
|||
# txtvalue must be url-encoded. But it's not necessary for acme txt value. |
|||
_freemyip_get_until_ok "${FREEMYIP_DNS_API}token=$FREEMYIP_Token&domain=$fulldomain&txt=$txtvalue" 2>&1 |
|||
return $? |
|||
} |
|||
|
|||
#Usage: dns_freemyip_rm fulldomain txtvalue |
|||
dns_freemyip_rm() { |
|||
fulldomain="$1" |
|||
txtvalue="$2" |
|||
|
|||
_info "Delete TXT record $txtvalue for $fulldomain using freemyip.com api" |
|||
|
|||
FREEMYIP_Token="${FREEMYIP_Token:-$(_readaccountconf_mutable FREEMYIP_Token)}" |
|||
if [ -z "$FREEMYIP_Token" ]; then |
|||
FREEMYIP_Token="" |
|||
_err "You don't specify FREEMYIP_Token yet." |
|||
_err "Please specify your token and try again." |
|||
return 1 |
|||
fi |
|||
|
|||
#save the credentials to the account conf file. |
|||
_saveaccountconf_mutable FREEMYIP_Token "$FREEMYIP_Token" |
|||
|
|||
# Leave the TXT record as empty or "null" to delete the record. |
|||
_freemyip_get_until_ok "${FREEMYIP_DNS_API}token=$FREEMYIP_Token&domain=$fulldomain&txt=" 2>&1 |
|||
return $? |
|||
} |
|||
|
|||
################ Private functions below ################ |
|||
_get_root() { |
|||
_fmi_d="$1" |
|||
|
|||
echo "$_fmi_d" | rev | cut -d '.' -f 1-3 | rev |
|||
} |
|||
|
|||
# There is random failure while calling freemyip API too fast. This function automatically retry until success. |
|||
_freemyip_get_until_ok() { |
|||
_fmi_url="$1" |
|||
for i in $(seq 1 8); do |
|||
_debug "HTTP GET freemyip.com API '$_fmi_url', retry $i/8..." |
|||
_get "$_fmi_url" | tee /dev/fd/2 | grep OK && return 0 |
|||
_sleep 1 # DO NOT send the request too fast |
|||
done |
|||
_err "Failed to request freemyip API: $_fmi_url . Server does not say 'OK'" |
|||
return 1 |
|||
} |
|||
|
|||
# Verify in public dns if domain is already there. |
|||
_is_root_domain_published() { |
|||
_fmi_d="$1" |
|||
_webroot="$(_get_root "$_fmi_d")" |
|||
|
|||
_info "Verifying '""$_fmi_d""' freemyip webroot (""$_webroot"") is not published yet" |
|||
for i in $(seq 1 3); do |
|||
_debug "'$_webroot' ns lookup, retry $i/3..." |
|||
if [ "$(_ns_lookup "$_fmi_d" TXT)" ]; then |
|||
_debug "'$_webroot' already has a TXT record published!" |
|||
return 0 |
|||
fi |
|||
_sleep 10 # Give it some time to propagate the TXT record |
|||
done |
|||
return 1 |
|||
} |
|||
@ -0,0 +1,45 @@ |
|||
#!/usr/bin/env sh |
|||
# shellcheck disable=SC2034 |
|||
dns_he_ddns_info='Hurricane Electric HE.net DDNS |
|||
Site: dns.he.net |
|||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_he_ddns |
|||
Options: |
|||
HE_DDNS_KEY The DDNS key |
|||
Issues: https://github.com/acmesh-official/acme.sh/issues/5238 |
|||
Author: Markku Leiniö |
|||
' |
|||
|
|||
HE_DDNS_URL="https://dyn.dns.he.net/nic/update" |
|||
|
|||
######## Public functions ##################### |
|||
|
|||
#Usage: dns_he_ddns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" |
|||
dns_he_ddns_add() { |
|||
fulldomain=$1 |
|||
txtvalue=$2 |
|||
HE_DDNS_KEY="${HE_DDNS_KEY:-$(_readaccountconf_mutable HE_DDNS_KEY)}" |
|||
if [ -z "$HE_DDNS_KEY" ]; then |
|||
HE_DDNS_KEY="" |
|||
_err "You didn't specify a DDNS key for accessing the TXT record in HE API." |
|||
return 1 |
|||
fi |
|||
#Save the DDNS key to the account conf file. |
|||
_saveaccountconf_mutable HE_DDNS_KEY "$HE_DDNS_KEY" |
|||
|
|||
_info "Using Hurricane Electric DDNS API" |
|||
_debug fulldomain "$fulldomain" |
|||
_debug txtvalue "$txtvalue" |
|||
|
|||
response="$(_post "hostname=$fulldomain&password=$HE_DDNS_KEY&txt=$txtvalue" "$HE_DDNS_URL")" |
|||
_info "Response: $response" |
|||
_contains "$response" "good" && return 0 || return 1 |
|||
} |
|||
|
|||
# dns_he_ddns_rm() is not doing anything because the API call always updates the |
|||
# contents of the existing record (that the API key gives access to). |
|||
|
|||
dns_he_ddns_rm() { |
|||
fulldomain=$1 |
|||
_debug "Delete TXT record called for '${fulldomain}', not doing anything." |
|||
return 0 |
|||
} |
|||
@ -0,0 +1,593 @@ |
|||
#!/usr/bin/env sh |
|||
# shellcheck disable=SC2034 |
|||
dns_hetznercloud_info='Hetzner Cloud DNS |
|||
Site: Hetzner.com |
|||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_hetznercloud |
|||
Options: |
|||
HETZNER_TOKEN API token for the Hetzner Cloud DNS API |
|||
Optional: |
|||
HETZNER_TTL Custom TTL for new TXT rrsets (default 120) |
|||
HETZNER_API Override API endpoint (default https://api.hetzner.cloud/v1) |
|||
HETZNER_MAX_ATTEMPTS Number of 1s polls to wait for async actions (default 120) |
|||
Issues: github.com/acmesh-official/acme.sh/issues |
|||
' |
|||
|
|||
HETZNERCLOUD_API_DEFAULT="https://api.hetzner.cloud/v1" |
|||
HETZNERCLOUD_TTL_DEFAULT=120 |
|||
HETZNER_MAX_ATTEMPTS_DEFAULT=120 |
|||
|
|||
######## Public functions ##################### |
|||
|
|||
dns_hetznercloud_add() { |
|||
fulldomain="$(_idn "${1}")" |
|||
txtvalue="${2}" |
|||
|
|||
_info "Using Hetzner Cloud DNS API to add record" |
|||
|
|||
if ! _hetznercloud_init; then |
|||
return 1 |
|||
fi |
|||
|
|||
if ! _hetznercloud_prepare_zone "${fulldomain}"; then |
|||
_err "Unable to determine Hetzner Cloud zone for ${fulldomain}" |
|||
return 1 |
|||
fi |
|||
|
|||
if ! _hetznercloud_get_rrset; then |
|||
return 1 |
|||
fi |
|||
|
|||
if [ "${_hetznercloud_last_http_code}" = "200" ]; then |
|||
if _hetznercloud_rrset_contains_value "${txtvalue}"; then |
|||
_info "TXT record already present; nothing to do." |
|||
return 0 |
|||
fi |
|||
elif [ "${_hetznercloud_last_http_code}" != "404" ]; then |
|||
_hetznercloud_log_http_error "Failed to query existing TXT rrset" "${_hetznercloud_last_http_code}" |
|||
return 1 |
|||
fi |
|||
|
|||
add_payload="$(_hetznercloud_build_add_payload "${txtvalue}")" |
|||
if [ -z "${add_payload}" ]; then |
|||
_err "Failed to build request payload." |
|||
return 1 |
|||
fi |
|||
|
|||
if ! _hetznercloud_api POST "${_hetznercloud_rrset_action_add}" "${add_payload}"; then |
|||
return 1 |
|||
fi |
|||
|
|||
case "${_hetznercloud_last_http_code}" in |
|||
200 | 201 | 202 | 204) |
|||
if ! _hetznercloud_handle_action_response "TXT record add"; then |
|||
return 1 |
|||
fi |
|||
_info "Hetzner Cloud TXT record added." |
|||
return 0 |
|||
;; |
|||
401 | 403) |
|||
_err "Hetzner Cloud DNS API authentication failed (HTTP ${_hetznercloud_last_http_code}). Check HETZNER_TOKEN for the new API." |
|||
_hetznercloud_log_http_error "" "${_hetznercloud_last_http_code}" |
|||
return 1 |
|||
;; |
|||
409 | 422) |
|||
_hetznercloud_log_http_error "Hetzner Cloud DNS rejected the add_records request" "${_hetznercloud_last_http_code}" |
|||
return 1 |
|||
;; |
|||
*) |
|||
_hetznercloud_log_http_error "Hetzner Cloud DNS add_records request failed" "${_hetznercloud_last_http_code}" |
|||
return 1 |
|||
;; |
|||
esac |
|||
} |
|||
|
|||
dns_hetznercloud_rm() { |
|||
fulldomain="$(_idn "${1}")" |
|||
txtvalue="${2}" |
|||
|
|||
_info "Using Hetzner Cloud DNS API to remove record" |
|||
|
|||
if ! _hetznercloud_init; then |
|||
return 1 |
|||
fi |
|||
|
|||
if ! _hetznercloud_prepare_zone "${fulldomain}"; then |
|||
_err "Unable to determine Hetzner Cloud zone for ${fulldomain}" |
|||
return 1 |
|||
fi |
|||
|
|||
if ! _hetznercloud_get_rrset; then |
|||
return 1 |
|||
fi |
|||
|
|||
if [ "${_hetznercloud_last_http_code}" = "404" ]; then |
|||
_info "TXT rrset does not exist; nothing to remove." |
|||
return 0 |
|||
fi |
|||
|
|||
if [ "${_hetznercloud_last_http_code}" != "200" ]; then |
|||
_hetznercloud_log_http_error "Failed to query existing TXT rrset" "${_hetznercloud_last_http_code}" |
|||
return 1 |
|||
fi |
|||
|
|||
if _hetznercloud_rrset_contains_value "${txtvalue}"; then |
|||
remove_payload="$(_hetznercloud_build_remove_payload "${txtvalue}")" |
|||
if [ -z "${remove_payload}" ]; then |
|||
_err "Failed to build remove_records payload." |
|||
return 1 |
|||
fi |
|||
if ! _hetznercloud_api POST "${_hetznercloud_rrset_action_remove}" "${remove_payload}"; then |
|||
return 1 |
|||
fi |
|||
case "${_hetznercloud_last_http_code}" in |
|||
200 | 201 | 202 | 204) |
|||
if ! _hetznercloud_handle_action_response "TXT record remove"; then |
|||
return 1 |
|||
fi |
|||
_info "Hetzner Cloud TXT record removed." |
|||
return 0 |
|||
;; |
|||
401 | 403) |
|||
_err "Hetzner Cloud DNS API authentication failed (HTTP ${_hetznercloud_last_http_code}). Check HETZNER_TOKEN for the new API." |
|||
_hetznercloud_log_http_error "" "${_hetznercloud_last_http_code}" |
|||
return 1 |
|||
;; |
|||
404) |
|||
_info "TXT rrset already absent after remove action." |
|||
return 0 |
|||
;; |
|||
409 | 422) |
|||
_hetznercloud_log_http_error "Hetzner Cloud DNS rejected the remove_records request" "${_hetznercloud_last_http_code}" |
|||
return 1 |
|||
;; |
|||
*) |
|||
_hetznercloud_log_http_error "Hetzner Cloud DNS remove_records request failed" "${_hetznercloud_last_http_code}" |
|||
return 1 |
|||
;; |
|||
esac |
|||
else |
|||
_info "TXT value not present; nothing to remove." |
|||
return 0 |
|||
fi |
|||
} |
|||
|
|||
#################### Private functions ################################## |
|||
|
|||
_hetznercloud_init() { |
|||
HETZNER_TOKEN="${HETZNER_TOKEN:-$(_readaccountconf_mutable HETZNER_TOKEN)}" |
|||
if [ -z "${HETZNER_TOKEN}" ]; then |
|||
_err "The environment variable HETZNER_TOKEN must be set for the Hetzner Cloud DNS API." |
|||
return 1 |
|||
fi |
|||
HETZNER_TOKEN=$(echo "${HETZNER_TOKEN}" | tr -d '"') |
|||
_saveaccountconf_mutable HETZNER_TOKEN "${HETZNER_TOKEN}" |
|||
|
|||
HETZNER_API="${HETZNER_API:-$(_readaccountconf_mutable HETZNER_API)}" |
|||
if [ -z "${HETZNER_API}" ]; then |
|||
HETZNER_API="${HETZNERCLOUD_API_DEFAULT}" |
|||
fi |
|||
_saveaccountconf_mutable HETZNER_API "${HETZNER_API}" |
|||
|
|||
HETZNER_TTL="${HETZNER_TTL:-$(_readaccountconf_mutable HETZNER_TTL)}" |
|||
if [ -z "${HETZNER_TTL}" ]; then |
|||
HETZNER_TTL="${HETZNERCLOUD_TTL_DEFAULT}" |
|||
fi |
|||
ttl_check=$(printf "%s" "${HETZNER_TTL}" | tr -d '0-9') |
|||
if [ -n "${ttl_check}" ]; then |
|||
_err "HETZNER_TTL must be an integer value." |
|||
return 1 |
|||
fi |
|||
_saveaccountconf_mutable HETZNER_TTL "${HETZNER_TTL}" |
|||
|
|||
HETZNER_MAX_ATTEMPTS="${HETZNER_MAX_ATTEMPTS:-$(_readaccountconf_mutable HETZNER_MAX_ATTEMPTS)}" |
|||
if [ -z "${HETZNER_MAX_ATTEMPTS}" ]; then |
|||
HETZNER_MAX_ATTEMPTS="${HETZNER_MAX_ATTEMPTS_DEFAULT}" |
|||
fi |
|||
attempts_check=$(printf "%s" "${HETZNER_MAX_ATTEMPTS}" | tr -d '0-9') |
|||
if [ -n "${attempts_check}" ]; then |
|||
_err "HETZNER_MAX_ATTEMPTS must be an integer value." |
|||
return 1 |
|||
fi |
|||
_saveaccountconf_mutable HETZNER_MAX_ATTEMPTS "${HETZNER_MAX_ATTEMPTS}" |
|||
|
|||
return 0 |
|||
} |
|||
|
|||
_hetznercloud_prepare_zone() { |
|||
_hetznercloud_zone_id="" |
|||
_hetznercloud_zone_name="" |
|||
_hetznercloud_zone_name_lc="" |
|||
_hetznercloud_rr_name="" |
|||
_hetznercloud_rrset_path="" |
|||
_hetznercloud_rrset_action_add="" |
|||
_hetznercloud_rrset_action_remove="" |
|||
fulldomain_lc=$(printf "%s" "${1}" | sed 's/\.$//' | _lower_case) |
|||
|
|||
i=2 |
|||
p=1 |
|||
while true; do |
|||
candidate=$(printf "%s" "${fulldomain_lc}" | cut -d . -f "${i}"-100) |
|||
if [ -z "${candidate}" ]; then |
|||
return 1 |
|||
fi |
|||
|
|||
if _hetznercloud_get_zone_by_candidate "${candidate}"; then |
|||
zone_name_lc="${_hetznercloud_zone_name_lc}" |
|||
if [ "${fulldomain_lc}" = "${zone_name_lc}" ]; then |
|||
_hetznercloud_rr_name="@" |
|||
else |
|||
suffix=".${zone_name_lc}" |
|||
if _endswith "${fulldomain_lc}" "${suffix}"; then |
|||
_hetznercloud_rr_name="${fulldomain_lc%"${suffix}"}" |
|||
else |
|||
_hetznercloud_rr_name="${fulldomain_lc}" |
|||
fi |
|||
fi |
|||
_hetznercloud_rrset_path=$(printf "%s" "${_hetznercloud_rr_name}" | _url_encode) |
|||
_hetznercloud_rrset_action_add="/zones/${_hetznercloud_zone_id}/rrsets/${_hetznercloud_rrset_path}/TXT/actions/add_records" |
|||
_hetznercloud_rrset_action_remove="/zones/${_hetznercloud_zone_id}/rrsets/${_hetznercloud_rrset_path}/TXT/actions/remove_records" |
|||
return 0 |
|||
fi |
|||
p=${i} |
|||
i=$(_math "${i}" + 1) |
|||
done |
|||
} |
|||
|
|||
_hetznercloud_get_zone_by_candidate() { |
|||
candidate="${1}" |
|||
zone_key=$(printf "%s" "${candidate}" | sed 's/[^A-Za-z0-9]/_/g') |
|||
zone_conf_key="HETZNERCLOUD_ZONE_ID_for_${zone_key}" |
|||
|
|||
cached_zone_id=$(_readdomainconf "${zone_conf_key}") |
|||
if [ -n "${cached_zone_id}" ]; then |
|||
if _hetznercloud_api GET "/zones/${cached_zone_id}"; then |
|||
if [ "${_hetznercloud_last_http_code}" = "200" ]; then |
|||
zone_data=$(printf "%s" "${response}" | _normalizeJson | sed 's/^{"zone"://' | sed 's/}$//') |
|||
if _hetznercloud_parse_zone_fields "${zone_data}"; then |
|||
zone_name_lc=$(printf "%s" "${_hetznercloud_zone_name}" | _lower_case) |
|||
if [ "${zone_name_lc}" = "${candidate}" ]; then |
|||
return 0 |
|||
fi |
|||
fi |
|||
elif [ "${_hetznercloud_last_http_code}" = "404" ]; then |
|||
_cleardomainconf "${zone_conf_key}" |
|||
fi |
|||
else |
|||
return 1 |
|||
fi |
|||
fi |
|||
|
|||
if _hetznercloud_api GET "/zones/${candidate}"; then |
|||
if [ "${_hetznercloud_last_http_code}" = "200" ]; then |
|||
zone_data=$(printf "%s" "${response}" | _normalizeJson | sed 's/^{"zone"://' | sed 's/}$//') |
|||
if _hetznercloud_parse_zone_fields "${zone_data}"; then |
|||
zone_name_lc=$(printf "%s" "${_hetznercloud_zone_name}" | _lower_case) |
|||
if [ "${zone_name_lc}" = "${candidate}" ]; then |
|||
_savedomainconf "${zone_conf_key}" "${_hetznercloud_zone_id}" |
|||
return 0 |
|||
fi |
|||
fi |
|||
elif [ "${_hetznercloud_last_http_code}" != "404" ]; then |
|||
_hetznercloud_log_http_error "Hetzner Cloud zone lookup failed" "${_hetznercloud_last_http_code}" |
|||
return 1 |
|||
fi |
|||
else |
|||
return 1 |
|||
fi |
|||
|
|||
encoded_candidate=$(printf "%s" "${candidate}" | _url_encode) |
|||
if ! _hetznercloud_api GET "/zones?name=${encoded_candidate}"; then |
|||
return 1 |
|||
fi |
|||
if [ "${_hetznercloud_last_http_code}" != "200" ]; then |
|||
if [ "${_hetznercloud_last_http_code}" = "404" ]; then |
|||
return 1 |
|||
fi |
|||
_hetznercloud_log_http_error "Hetzner Cloud zone search failed" "${_hetznercloud_last_http_code}" |
|||
return 1 |
|||
fi |
|||
|
|||
zone_data=$(_hetznercloud_extract_zone_from_list "${response}" "${candidate}") |
|||
if [ -z "${zone_data}" ]; then |
|||
return 1 |
|||
fi |
|||
if ! _hetznercloud_parse_zone_fields "${zone_data}"; then |
|||
return 1 |
|||
fi |
|||
_savedomainconf "${zone_conf_key}" "${_hetznercloud_zone_id}" |
|||
return 0 |
|||
} |
|||
|
|||
_hetznercloud_parse_zone_fields() { |
|||
zone_json="${1}" |
|||
if [ -z "${zone_json}" ]; then |
|||
return 1 |
|||
fi |
|||
normalized=$(printf "%s" "${zone_json}" | _normalizeJson) |
|||
zone_id=$(printf "%s" "${normalized}" | _egrep_o '"id":[^,}]*' | _head_n 1 | cut -d : -f 2 | tr -d ' "') |
|||
zone_name=$(printf "%s" "${normalized}" | _egrep_o '"name":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"') |
|||
if [ -z "${zone_id}" ] || [ -z "${zone_name}" ]; then |
|||
return 1 |
|||
fi |
|||
zone_name_trimmed=$(printf "%s" "${zone_name}" | sed 's/\.$//') |
|||
if zone_name_ascii=$(_idn "${zone_name_trimmed}"); then |
|||
zone_name="${zone_name_ascii}" |
|||
else |
|||
zone_name="${zone_name_trimmed}" |
|||
fi |
|||
_hetznercloud_zone_id="${zone_id}" |
|||
_hetznercloud_zone_name="${zone_name}" |
|||
_hetznercloud_zone_name_lc=$(printf "%s" "${zone_name}" | _lower_case) |
|||
return 0 |
|||
} |
|||
|
|||
_hetznercloud_extract_zone_from_list() { |
|||
list_response=$(printf "%s" "${1}" | _normalizeJson) |
|||
candidate="${2}" |
|||
escaped_candidate=$(_hetznercloud_escape_regex "${candidate}") |
|||
printf "%s" "${list_response}" | _egrep_o "{[^{}]*\"name\":\"${escaped_candidate}\"[^{}]*}" | _head_n 1 |
|||
} |
|||
|
|||
_hetznercloud_escape_regex() { |
|||
printf "%s" "${1}" | sed 's/\\/\\\\/g' | sed 's/\./\\./g' | sed 's/-/\\-/g' |
|||
} |
|||
|
|||
_hetznercloud_get_rrset() { |
|||
if [ -z "${_hetznercloud_zone_id}" ] || [ -z "${_hetznercloud_rrset_path}" ]; then |
|||
return 1 |
|||
fi |
|||
if ! _hetznercloud_api GET "/zones/${_hetznercloud_zone_id}/rrsets/${_hetznercloud_rrset_path}/TXT"; then |
|||
return 1 |
|||
fi |
|||
return 0 |
|||
} |
|||
|
|||
_hetznercloud_rrset_contains_value() { |
|||
wanted_value="${1}" |
|||
normalized=$(printf "%s" "${response}" | _normalizeJson) |
|||
escaped_value=$(_hetznercloud_escape_value "${wanted_value}") |
|||
search_pattern="\"value\":\"\\\\\"${escaped_value}\\\\\"\"" |
|||
if _contains "${normalized}" "${search_pattern}"; then |
|||
return 0 |
|||
fi |
|||
return 1 |
|||
} |
|||
|
|||
_hetznercloud_build_add_payload() { |
|||
value="${1}" |
|||
escaped_value=$(_hetznercloud_escape_value "${value}") |
|||
printf '{"ttl":%s,"records":[{"value":"\\"%s\\""}]}' "${HETZNER_TTL}" "${escaped_value}" |
|||
} |
|||
|
|||
_hetznercloud_build_remove_payload() { |
|||
value="${1}" |
|||
escaped_value=$(_hetznercloud_escape_value "${value}") |
|||
printf '{"records":[{"value":"\\"%s\\""}]}' "${escaped_value}" |
|||
} |
|||
|
|||
_hetznercloud_escape_value() { |
|||
printf "%s" "${1}" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' |
|||
} |
|||
|
|||
_hetznercloud_error_message() { |
|||
if [ -z "${response}" ]; then |
|||
return 1 |
|||
fi |
|||
message=$(printf "%s" "${response}" | _normalizeJson | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"') |
|||
if [ -n "${message}" ]; then |
|||
printf "%s" "${message}" |
|||
return 0 |
|||
fi |
|||
return 1 |
|||
} |
|||
|
|||
_hetznercloud_log_http_error() { |
|||
context="${1}" |
|||
code="${2}" |
|||
message="$(_hetznercloud_error_message)" |
|||
if [ -n "${context}" ]; then |
|||
if [ -n "${message}" ]; then |
|||
_err "${context} (HTTP ${code}): ${message}" |
|||
else |
|||
_err "${context} (HTTP ${code})" |
|||
fi |
|||
else |
|||
if [ -n "${message}" ]; then |
|||
_err "Hetzner Cloud DNS API error (HTTP ${code}): ${message}" |
|||
else |
|||
_err "Hetzner Cloud DNS API error (HTTP ${code})" |
|||
fi |
|||
fi |
|||
} |
|||
|
|||
_hetznercloud_api() { |
|||
method="${1}" |
|||
ep="${2}" |
|||
data="${3}" |
|||
retried="${4}" |
|||
|
|||
if [ -z "${method}" ]; then |
|||
method="GET" |
|||
fi |
|||
|
|||
if ! _startswith "${ep}" "/"; then |
|||
ep="/${ep}" |
|||
fi |
|||
url="${HETZNER_API}${ep}" |
|||
|
|||
export _H1="Authorization: Bearer ${HETZNER_TOKEN}" |
|||
export _H2="Accept: application/json" |
|||
export _H3="" |
|||
export _H4="" |
|||
export _H5="" |
|||
|
|||
: >"${HTTP_HEADER}" |
|||
|
|||
if [ "${method}" = "GET" ]; then |
|||
response="$(_get "${url}")" |
|||
else |
|||
if [ -z "${data}" ]; then |
|||
data="{}" |
|||
fi |
|||
response="$(_post "${data}" "${url}" "" "${method}" "application/json")" |
|||
fi |
|||
ret="${?}" |
|||
|
|||
_hetznercloud_last_http_code=$(grep "^HTTP" "${HTTP_HEADER}" | _tail_n 1 | cut -d " " -f 2 | tr -d '\r\n') |
|||
|
|||
if [ "${ret}" != "0" ]; then |
|||
return 1 |
|||
fi |
|||
|
|||
if [ "${_hetznercloud_last_http_code}" = "429" ] && [ "${retried}" != "retried" ]; then |
|||
retry_after=$(grep -i "^Retry-After" "${HTTP_HEADER}" | _tail_n 1 | cut -d : -f 2 | tr -d ' \r') |
|||
if [ -z "${retry_after}" ]; then |
|||
retry_after=1 |
|||
fi |
|||
_info "Hetzner Cloud DNS API rate limit hit; retrying in ${retry_after} seconds." |
|||
_sleep "${retry_after}" |
|||
if ! _hetznercloud_api "${method}" "${ep}" "${data}" "retried"; then |
|||
return 1 |
|||
fi |
|||
return 0 |
|||
fi |
|||
|
|||
return 0 |
|||
} |
|||
|
|||
_hetznercloud_handle_action_response() { |
|||
context="${1}" |
|||
if [ -z "${response}" ]; then |
|||
return 0 |
|||
fi |
|||
|
|||
normalized=$(printf "%s" "${response}" | _normalizeJson) |
|||
|
|||
failed_message="" |
|||
if failed_message=$(_hetznercloud_extract_failed_action_message "${normalized}"); then |
|||
if [ -n "${failed_message}" ]; then |
|||
_err "Hetzner Cloud DNS ${context} failed: ${failed_message}" |
|||
else |
|||
_err "Hetzner Cloud DNS ${context} failed." |
|||
fi |
|||
return 1 |
|||
fi |
|||
|
|||
action_ids="" |
|||
if action_ids=$(_hetznercloud_extract_action_ids "${normalized}"); then |
|||
for action_id in ${action_ids}; do |
|||
if [ -z "${action_id}" ]; then |
|||
continue |
|||
fi |
|||
if ! _hetznercloud_wait_for_action "${action_id}" "${context}"; then |
|||
return 1 |
|||
fi |
|||
done |
|||
fi |
|||
|
|||
return 0 |
|||
} |
|||
|
|||
_hetznercloud_extract_failed_action_message() { |
|||
normalized="${1}" |
|||
failed_section=$(printf "%s" "${normalized}" | _egrep_o '"failed_actions":\[[^]]*\]') |
|||
if [ -z "${failed_section}" ]; then |
|||
return 1 |
|||
fi |
|||
if _contains "${failed_section}" '"failed_actions":[]'; then |
|||
return 1 |
|||
fi |
|||
message=$(printf "%s" "${failed_section}" | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"') |
|||
if [ -n "${message}" ]; then |
|||
printf "%s" "${message}" |
|||
else |
|||
printf "%s" "${failed_section}" |
|||
fi |
|||
return 0 |
|||
} |
|||
|
|||
_hetznercloud_extract_action_ids() { |
|||
normalized="${1}" |
|||
actions_section=$(printf "%s" "${normalized}" | _egrep_o '"actions":\[[^]]*\]') |
|||
if [ -z "${actions_section}" ]; then |
|||
return 1 |
|||
fi |
|||
action_ids=$(printf "%s" "${actions_section}" | _egrep_o '"id":[0-9]*' | cut -d : -f 2 | tr -d '"' | tr '\n' ' ') |
|||
action_ids=$(printf "%s" "${action_ids}" | tr -s ' ') |
|||
action_ids=$(printf "%s" "${action_ids}" | sed 's/^ //;s/ $//') |
|||
if [ -z "${action_ids}" ]; then |
|||
return 1 |
|||
fi |
|||
printf "%s" "${action_ids}" |
|||
return 0 |
|||
} |
|||
|
|||
_hetznercloud_wait_for_action() { |
|||
action_id="${1}" |
|||
context="${2}" |
|||
attempts="0" |
|||
|
|||
while true; do |
|||
if ! _hetznercloud_api GET "/actions/${action_id}"; then |
|||
return 1 |
|||
fi |
|||
if [ "${_hetznercloud_last_http_code}" != "200" ]; then |
|||
_hetznercloud_log_http_error "Hetzner Cloud DNS action ${action_id} query failed" "${_hetznercloud_last_http_code}" |
|||
return 1 |
|||
fi |
|||
|
|||
normalized=$(printf "%s" "${response}" | _normalizeJson) |
|||
action_status=$(_hetznercloud_action_status_from_normalized "${normalized}") |
|||
|
|||
if [ -z "${action_status}" ]; then |
|||
_err "Hetzner Cloud DNS ${context} action ${action_id} returned no status." |
|||
return 1 |
|||
fi |
|||
|
|||
if [ "${action_status}" = "success" ]; then |
|||
return 0 |
|||
fi |
|||
|
|||
if [ "${action_status}" = "error" ]; then |
|||
if action_error=$(_hetznercloud_action_error_from_normalized "${normalized}"); then |
|||
_err "Hetzner Cloud DNS ${context} action ${action_id} failed: ${action_error}" |
|||
else |
|||
_err "Hetzner Cloud DNS ${context} action ${action_id} failed." |
|||
fi |
|||
return 1 |
|||
fi |
|||
|
|||
attempts=$(_math "${attempts}" + 1) |
|||
if [ "${attempts}" -ge "${HETZNER_MAX_ATTEMPTS}" ]; then |
|||
_err "Hetzner Cloud DNS ${context} action ${action_id} did not complete after ${HETZNER_MAX_ATTEMPTS} attempts." |
|||
return 1 |
|||
fi |
|||
|
|||
_sleep 1 |
|||
done |
|||
} |
|||
|
|||
_hetznercloud_action_status_from_normalized() { |
|||
normalized="${1}" |
|||
status=$(printf "%s" "${normalized}" | _egrep_o '"status":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"') |
|||
printf "%s" "${status}" |
|||
} |
|||
|
|||
_hetznercloud_action_error_from_normalized() { |
|||
normalized="${1}" |
|||
error_section=$(printf "%s" "${normalized}" | _egrep_o '"error":{[^}]*}') |
|||
if [ -z "${error_section}" ]; then |
|||
return 1 |
|||
fi |
|||
message=$(printf "%s" "${error_section}" | _egrep_o '"message":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"') |
|||
if [ -n "${message}" ]; then |
|||
printf "%s" "${message}" |
|||
return 0 |
|||
fi |
|||
code=$(printf "%s" "${error_section}" | _egrep_o '"code":"[^"]*"' | _head_n 1 | cut -d : -f 2 | tr -d '"') |
|||
if [ -n "${code}" ]; then |
|||
printf "%s" "${code}" |
|||
return 0 |
|||
fi |
|||
return 1 |
|||
} |
|||
@ -0,0 +1,501 @@ |
|||
#!/usr/bin/env sh |
|||
# shellcheck disable=SC2034,SC2154 |
|||
|
|||
dns_hostup_info='HostUp DNS |
|||
Site: hostup.se |
|||
Docs: https://developer.hostup.se/ |
|||
Options: |
|||
HOSTUP_API_KEY Required. HostUp API key with read:dns + write:dns + read:domains scopes. |
|||
HOSTUP_API_BASE Optional. Override API base URL (default: https://cloud.hostup.se/api). |
|||
HOSTUP_TTL Optional. TTL for TXT records (default: 60 seconds). |
|||
HOSTUP_ZONE_ID Optional. Force a specific zone ID (skip auto-detection). |
|||
Author: HostUp (https://cloud.hostup.se/contact/en) |
|||
' |
|||
|
|||
HOSTUP_API_BASE_DEFAULT="https://cloud.hostup.se/api" |
|||
HOSTUP_DEFAULT_TTL=60 |
|||
|
|||
# Public: add TXT record |
|||
# Usage: dns_hostup_add _acme-challenge.example.com "txt-value" |
|||
dns_hostup_add() { |
|||
fulldomain="$1" |
|||
txtvalue="$2" |
|||
|
|||
_info "Using HostUp DNS API" |
|||
|
|||
if ! _hostup_init; then |
|||
return 1 |
|||
fi |
|||
|
|||
if ! _hostup_detect_zone "$fulldomain"; then |
|||
_err "Unable to determine HostUp zone for $fulldomain" |
|||
return 1 |
|||
fi |
|||
|
|||
record_name="$(_hostup_record_name "$fulldomain" "$HOSTUP_ZONE_DOMAIN")" |
|||
record_name="$(_hostup_sanitize_name "$record_name")" |
|||
record_value="$(_hostup_json_escape "$txtvalue")" |
|||
|
|||
ttl="${HOSTUP_TTL:-$HOSTUP_DEFAULT_TTL}" |
|||
|
|||
_debug "zone_id" "$HOSTUP_ZONE_ID" |
|||
_debug "zone_domain" "$HOSTUP_ZONE_DOMAIN" |
|||
_debug "record_name" "$record_name" |
|||
_debug "ttl" "$ttl" |
|||
|
|||
request_body="{\"name\":\"$record_name\",\"type\":\"TXT\",\"value\":\"$record_value\",\"ttl\":$ttl}" |
|||
|
|||
if ! _hostup_rest "POST" "/dns/zones/$HOSTUP_ZONE_ID/records" "$request_body"; then |
|||
return 1 |
|||
fi |
|||
|
|||
if ! _contains "$_hostup_response" '"success":true'; then |
|||
_err "HostUp DNS API: failed to create TXT record for $fulldomain" |
|||
_debug2 "_hostup_response" "$_hostup_response" |
|||
return 1 |
|||
fi |
|||
|
|||
record_id="$(_hostup_extract_record_id "$_hostup_response")" |
|||
if [ -n "$record_id" ]; then |
|||
_hostup_save_record_id "$HOSTUP_ZONE_ID" "$fulldomain" "$record_id" |
|||
_debug "hostup_saved_record_id" "$record_id" |
|||
fi |
|||
|
|||
_info "Added TXT record for $fulldomain" |
|||
return 0 |
|||
} |
|||
|
|||
# Public: remove TXT record |
|||
# Usage: dns_hostup_rm _acme-challenge.example.com "txt-value" |
|||
dns_hostup_rm() { |
|||
fulldomain="$1" |
|||
txtvalue="$2" |
|||
|
|||
_info "Using HostUp DNS API" |
|||
|
|||
if ! _hostup_init; then |
|||
return 1 |
|||
fi |
|||
|
|||
if ! _hostup_detect_zone "$fulldomain"; then |
|||
_err "Unable to determine HostUp zone for $fulldomain" |
|||
return 1 |
|||
fi |
|||
|
|||
record_name_fqdn="$(_hostup_fqdn "$fulldomain")" |
|||
record_value="$txtvalue" |
|||
|
|||
record_id_cached="$(_hostup_get_saved_record_id "$HOSTUP_ZONE_ID" "$fulldomain")" |
|||
if [ -n "$record_id_cached" ]; then |
|||
_debug "hostup_record_id_cached" "$record_id_cached" |
|||
if _hostup_delete_record_by_id "$HOSTUP_ZONE_ID" "$record_id_cached"; then |
|||
_info "Deleted TXT record $record_id_cached" |
|||
_hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain" |
|||
HOSTUP_ZONE_ID="" |
|||
return 0 |
|||
fi |
|||
fi |
|||
|
|||
if ! _hostup_find_record "$HOSTUP_ZONE_ID" "$record_name_fqdn" "$record_value"; then |
|||
_info "TXT record not found for $record_name_fqdn. Skipping removal." |
|||
_hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain" |
|||
return 0 |
|||
fi |
|||
|
|||
_debug "Deleting record" "$HOSTUP_RECORD_ID" |
|||
|
|||
if ! _hostup_delete_record_by_id "$HOSTUP_ZONE_ID" "$HOSTUP_RECORD_ID"; then |
|||
return 1 |
|||
fi |
|||
|
|||
_info "Deleted TXT record $HOSTUP_RECORD_ID" |
|||
_hostup_clear_record_id "$HOSTUP_ZONE_ID" "$fulldomain" |
|||
HOSTUP_ZONE_ID="" |
|||
return 0 |
|||
} |
|||
|
|||
########################## |
|||
# Private helper methods # |
|||
########################## |
|||
|
|||
_hostup_init() { |
|||
HOSTUP_API_KEY="${HOSTUP_API_KEY:-$(_readaccountconf_mutable HOSTUP_API_KEY)}" |
|||
HOSTUP_API_BASE="${HOSTUP_API_BASE:-$(_readaccountconf_mutable HOSTUP_API_BASE)}" |
|||
HOSTUP_TTL="${HOSTUP_TTL:-$(_readaccountconf_mutable HOSTUP_TTL)}" |
|||
HOSTUP_ZONE_ID="${HOSTUP_ZONE_ID:-$(_readaccountconf_mutable HOSTUP_ZONE_ID)}" |
|||
|
|||
if [ -z "$HOSTUP_API_BASE" ]; then |
|||
HOSTUP_API_BASE="$HOSTUP_API_BASE_DEFAULT" |
|||
fi |
|||
|
|||
if [ -z "$HOSTUP_API_KEY" ]; then |
|||
HOSTUP_API_KEY="" |
|||
_err "HOSTUP_API_KEY is not set." |
|||
_err "Please export your HostUp API key with read:dns and write:dns scopes." |
|||
return 1 |
|||
fi |
|||
|
|||
_saveaccountconf_mutable HOSTUP_API_KEY "$HOSTUP_API_KEY" |
|||
_saveaccountconf_mutable HOSTUP_API_BASE "$HOSTUP_API_BASE" |
|||
|
|||
if [ -n "$HOSTUP_TTL" ]; then |
|||
_saveaccountconf_mutable HOSTUP_TTL "$HOSTUP_TTL" |
|||
fi |
|||
|
|||
if [ -n "$HOSTUP_ZONE_ID" ]; then |
|||
_saveaccountconf_mutable HOSTUP_ZONE_ID "$HOSTUP_ZONE_ID" |
|||
fi |
|||
|
|||
return 0 |
|||
} |
|||
|
|||
_hostup_detect_zone() { |
|||
fulldomain="$1" |
|||
|
|||
if [ -n "$HOSTUP_ZONE_ID" ] && [ -n "$HOSTUP_ZONE_DOMAIN" ]; then |
|||
return 0 |
|||
fi |
|||
|
|||
HOSTUP_ZONE_DOMAIN="" |
|||
_debug "hostup_full_domain" "$fulldomain" |
|||
|
|||
if [ -n "$HOSTUP_ZONE_ID" ] && [ -z "$HOSTUP_ZONE_DOMAIN" ]; then |
|||
# Attempt to fetch domain name for provided zone ID |
|||
if _hostup_fetch_zone_details "$HOSTUP_ZONE_ID"; then |
|||
return 0 |
|||
fi |
|||
HOSTUP_ZONE_ID="" |
|||
fi |
|||
|
|||
if ! _hostup_load_zones; then |
|||
return 1 |
|||
fi |
|||
|
|||
_domain_candidate="$(printf "%s" "$fulldomain" | _lower_case)" |
|||
_debug "hostup_initial_candidate" "$_domain_candidate" |
|||
|
|||
while [ -n "$_domain_candidate" ]; do |
|||
_debug "hostup_zone_candidate" "$_domain_candidate" |
|||
if _hostup_lookup_zone "$_domain_candidate"; then |
|||
HOSTUP_ZONE_DOMAIN="$_lookup_zone_domain" |
|||
HOSTUP_ZONE_ID="$_lookup_zone_id" |
|||
return 0 |
|||
fi |
|||
|
|||
case "$_domain_candidate" in |
|||
*.*) ;; |
|||
*) break ;; |
|||
esac |
|||
|
|||
_domain_candidate="${_domain_candidate#*.}" |
|||
done |
|||
|
|||
HOSTUP_ZONE_ID="" |
|||
return 1 |
|||
} |
|||
|
|||
_hostup_record_name() { |
|||
fulldomain="$1" |
|||
zonedomain="$2" |
|||
|
|||
# Remove trailing dot, if any |
|||
fulldomain="${fulldomain%.}" |
|||
zonedomain="${zonedomain%.}" |
|||
|
|||
if [ "$fulldomain" = "$zonedomain" ]; then |
|||
printf "%s" "@" |
|||
return 0 |
|||
fi |
|||
|
|||
suffix=".$zonedomain" |
|||
case "$fulldomain" in |
|||
*"$suffix") |
|||
printf "%s" "${fulldomain%"$suffix"}" |
|||
;; |
|||
*) |
|||
# Domain not within zone, fall back to full host |
|||
printf "%s" "$fulldomain" |
|||
;; |
|||
esac |
|||
} |
|||
|
|||
_hostup_sanitize_name() { |
|||
name="$1" |
|||
|
|||
if [ -z "$name" ] || [ "$name" = "." ]; then |
|||
printf "%s" "@" |
|||
return 0 |
|||
fi |
|||
|
|||
# Remove any trailing dot |
|||
name="${name%.}" |
|||
printf "%s" "$name" |
|||
} |
|||
|
|||
_hostup_fqdn() { |
|||
domain="$1" |
|||
printf "%s" "${domain%.}" |
|||
} |
|||
|
|||
_hostup_fetch_zone_details() { |
|||
zone_id="$1" |
|||
|
|||
if ! _hostup_rest "GET" "/dns/zones/$zone_id/records" ""; then |
|||
return 1 |
|||
fi |
|||
|
|||
zonedomain="$(printf "%s" "$_hostup_response" | _egrep_o '"domain":"[^"]*"' | sed -n '1p' | cut -d ':' -f 2 | tr -d '"')" |
|||
if [ -n "$zonedomain" ]; then |
|||
HOSTUP_ZONE_DOMAIN="$zonedomain" |
|||
return 0 |
|||
fi |
|||
|
|||
return 1 |
|||
} |
|||
|
|||
_hostup_load_zones() { |
|||
if ! _hostup_rest "GET" "/dns/zones" ""; then |
|||
return 1 |
|||
fi |
|||
|
|||
HOSTUP_ZONES_CACHE="" |
|||
data="$(printf "%s" "$_hostup_response" | tr '{' '\n')" |
|||
|
|||
while IFS= read -r line; do |
|||
case "$line" in |
|||
*'"domain_id"'*'"domain"'*) |
|||
zone_id="$(printf "%s" "$line" | _hostup_json_extract "domain_id")" |
|||
zone_domain="$(printf "%s" "$line" | _hostup_json_extract "domain")" |
|||
if [ -n "$zone_id" ] && [ -n "$zone_domain" ]; then |
|||
HOSTUP_ZONES_CACHE="${HOSTUP_ZONES_CACHE}${zone_domain}|${zone_id} |
|||
" |
|||
_debug "hostup_zone_loaded" "$zone_domain|$zone_id" |
|||
fi |
|||
;; |
|||
esac |
|||
done <<EOF |
|||
$data |
|||
EOF |
|||
|
|||
if [ -z "$HOSTUP_ZONES_CACHE" ]; then |
|||
_err "HostUp DNS API: no zones returned for the current API key." |
|||
return 1 |
|||
fi |
|||
|
|||
return 0 |
|||
} |
|||
|
|||
_hostup_lookup_zone() { |
|||
lookup_domain="$1" |
|||
_lookup_zone_id="" |
|||
_lookup_zone_domain="" |
|||
|
|||
while IFS='|' read -r domain zone_id; do |
|||
[ -z "$domain" ] && continue |
|||
if [ "$domain" = "$lookup_domain" ]; then |
|||
_lookup_zone_domain="$domain" |
|||
_lookup_zone_id="$zone_id" |
|||
HOSTUP_ZONE_DOMAIN="$domain" |
|||
HOSTUP_ZONE_ID="$zone_id" |
|||
return 0 |
|||
fi |
|||
done <<EOF |
|||
$HOSTUP_ZONES_CACHE |
|||
EOF |
|||
|
|||
return 1 |
|||
} |
|||
|
|||
_hostup_find_record() { |
|||
zone_id="$1" |
|||
fqdn="$2" |
|||
txtvalue="$3" |
|||
|
|||
if ! _hostup_rest "GET" "/dns/zones/$zone_id/records" ""; then |
|||
return 1 |
|||
fi |
|||
|
|||
HOSTUP_RECORD_ID="" |
|||
records="$(printf "%s" "$_hostup_response" | tr '{' '\n')" |
|||
|
|||
while IFS= read -r line; do |
|||
# Normalize line to make TXT value matching reliable |
|||
line_clean="$(printf "%s" "$line" | tr -d '\r\n')" |
|||
line_value_clean="$(printf "%s" "$line_clean" | sed 's/\\"//g')" |
|||
|
|||
case "$line_clean" in |
|||
*'"type":"TXT"'*'"name"'*'"value"'*) |
|||
name_value="$(_hostup_json_extract "name" "$line_clean")" |
|||
record_value="$(_hostup_json_extract "value" "$line_value_clean")" |
|||
|
|||
_debug "hostup_record_raw" "$record_value" |
|||
if [ "${record_value#\"}" != "$record_value" ] && [ "${record_value%\"}" != "$record_value" ]; then |
|||
record_value="${record_value#\"}" |
|||
record_value="${record_value%\"}" |
|||
fi |
|||
if [ "${record_value#\'}" != "$record_value" ] && [ "${record_value%\'}" != "$record_value" ]; then |
|||
record_value="${record_value#\'}" |
|||
record_value="${record_value%\'}" |
|||
fi |
|||
record_value="$(printf "%s" "$record_value" | tr -d '\r\n')" |
|||
_debug "hostup_record_value" "$record_value" |
|||
|
|||
if [ "$name_value" = "$fqdn" ] && [ "$record_value" = "$txtvalue" ]; then |
|||
record_id="$(_hostup_json_extract "id" "$line_clean")" |
|||
if [ -n "$record_id" ]; then |
|||
HOSTUP_RECORD_ID="$record_id" |
|||
return 0 |
|||
fi |
|||
fi |
|||
;; |
|||
esac |
|||
done <<EOF |
|||
$records |
|||
EOF |
|||
|
|||
return 1 |
|||
} |
|||
|
|||
_hostup_json_extract() { |
|||
key="$1" |
|||
input="${2:-$line}" |
|||
|
|||
# First try to extract quoted values (strings) |
|||
quoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\":\"[^\"]*\"" | _head_n 1)" |
|||
if [ -n "$quoted_match" ]; then |
|||
printf "%s" "$quoted_match" | |
|||
cut -d : -f2- | |
|||
sed 's/^"//' | |
|||
sed 's/"$//' | |
|||
sed 's/\\"/"/g' |
|||
return 0 |
|||
fi |
|||
|
|||
# Fallback for unquoted values (e.g., numeric IDs) |
|||
unquoted_match="$(printf "%s" "$input" | _egrep_o "\"$key\":[^,}]*" | _head_n 1)" |
|||
if [ -n "$unquoted_match" ]; then |
|||
printf "%s" "$unquoted_match" | |
|||
cut -d : -f2- | |
|||
tr -d '", ' | |
|||
tr -d '\r\n' |
|||
return 0 |
|||
fi |
|||
|
|||
return 1 |
|||
} |
|||
|
|||
_hostup_json_escape() { |
|||
printf "%s" "$1" | sed 's/\\/\\\\/g; s/"/\\"/g' |
|||
} |
|||
|
|||
_hostup_record_key() { |
|||
zone_id="$1" |
|||
domain="$2" |
|||
safe_zone="$(printf "%s" "$zone_id" | sed 's/[^A-Za-z0-9]/_/g')" |
|||
safe_domain="$(printf "%s" "$domain" | _lower_case | sed 's/[^a-z0-9]/_/g')" |
|||
printf "%s_%s" "$safe_zone" "$safe_domain" |
|||
} |
|||
|
|||
_hostup_save_record_id() { |
|||
zone_id="$1" |
|||
domain="$2" |
|||
record_id="$3" |
|||
key="$(_hostup_record_key "$zone_id" "$domain")" |
|||
_saveaccountconf_mutable "HOSTUP_RECORD_$key" "$record_id" |
|||
} |
|||
|
|||
_hostup_get_saved_record_id() { |
|||
zone_id="$1" |
|||
domain="$2" |
|||
key="$(_hostup_record_key "$zone_id" "$domain")" |
|||
_readaccountconf_mutable "HOSTUP_RECORD_$key" |
|||
} |
|||
|
|||
_hostup_clear_record_id() { |
|||
zone_id="$1" |
|||
domain="$2" |
|||
key="$(_hostup_record_key "$zone_id" "$domain")" |
|||
_clearaccountconf_mutable "HOSTUP_RECORD_$key" |
|||
} |
|||
|
|||
_hostup_extract_record_id() { |
|||
record_id="$(_hostup_json_extract "id" "$1")" |
|||
if [ -n "$record_id" ]; then |
|||
printf "%s" "$record_id" |
|||
return 0 |
|||
fi |
|||
|
|||
printf "%s" "$1" | _egrep_o '"id":[0-9]+' | _head_n 1 | cut -d: -f2 |
|||
} |
|||
|
|||
_hostup_delete_record_by_id() { |
|||
zone_id="$1" |
|||
record_id="$2" |
|||
|
|||
if ! _hostup_rest "DELETE" "/dns/zones/$zone_id/records/$record_id" ""; then |
|||
return 1 |
|||
fi |
|||
|
|||
if ! _contains "$_hostup_response" '"success":true'; then |
|||
return 1 |
|||
fi |
|||
|
|||
return 0 |
|||
} |
|||
|
|||
_hostup_rest() { |
|||
method="$1" |
|||
route="$2" |
|||
data="$3" |
|||
|
|||
_hostup_response="" |
|||
|
|||
export _H1="Authorization: Bearer $HOSTUP_API_KEY" |
|||
export _H2="Content-Type: application/json" |
|||
export _H3="Accept: application/json" |
|||
|
|||
if [ "$method" = "GET" ]; then |
|||
_hostup_response="$(_get "$HOSTUP_API_BASE$route")" |
|||
else |
|||
_hostup_response="$(_post "$data" "$HOSTUP_API_BASE$route" "" "$method" "application/json")" |
|||
fi |
|||
|
|||
ret="$?" |
|||
|
|||
unset _H1 |
|||
unset _H2 |
|||
unset _H3 |
|||
|
|||
if [ "$ret" != "0" ]; then |
|||
_err "HTTP request failed for $route" |
|||
return 1 |
|||
fi |
|||
|
|||
http_status="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\r\n")" |
|||
_debug2 "HTTP status" "$http_status" |
|||
_debug2 "_hostup_response" "$_hostup_response" |
|||
|
|||
case "$http_status" in |
|||
200 | 201 | 204) return 0 ;; |
|||
401) |
|||
_err "HostUp API returned 401 Unauthorized. Check HOSTUP_API_KEY scopes and IP restrictions." |
|||
return 1 |
|||
;; |
|||
403) |
|||
_err "HostUp API returned 403 Forbidden. The API key lacks required DNS scopes." |
|||
return 1 |
|||
;; |
|||
404) |
|||
_err "HostUp API returned 404 Not Found for $route" |
|||
return 1 |
|||
;; |
|||
429) |
|||
_err "HostUp API rate limit exceeded. Please retry later." |
|||
return 1 |
|||
;; |
|||
*) |
|||
_err "HostUp API request failed with status $http_status" |
|||
return 1 |
|||
;; |
|||
esac |
|||
} |
|||
@ -0,0 +1,244 @@ |
|||
#!/usr/bin/env sh |
|||
# shellcheck disable=SC2034 |
|||
dns_infoblox_uddi_info='Infoblox UDDI |
|||
Site: Infoblox.com |
|||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_infoblox_uddi |
|||
Options: |
|||
Infoblox_UDDI_Key API Key for Infoblox UDDI |
|||
Infoblox_Portal URL, e.g. "csp.infoblox.com" or "csp.eu.infoblox.com" |
|||
Issues: github.com/acmesh-official/acme.sh/issues |
|||
Author: Stefan Riegel |
|||
' |
|||
|
|||
Infoblox_UDDI_Api="https://" |
|||
|
|||
######## Public functions ##################### |
|||
|
|||
#Usage: dns_infoblox_uddi_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" |
|||
dns_infoblox_uddi_add() { |
|||
fulldomain=$1 |
|||
txtvalue=$2 |
|||
|
|||
Infoblox_UDDI_Key="${Infoblox_UDDI_Key:-$(_readaccountconf_mutable Infoblox_UDDI_Key)}" |
|||
Infoblox_Portal="${Infoblox_Portal:-$(_readaccountconf_mutable Infoblox_Portal)}" |
|||
|
|||
_info "Using Infoblox UDDI API" |
|||
_debug fulldomain "$fulldomain" |
|||
_debug txtvalue "$txtvalue" |
|||
|
|||
if [ -z "$Infoblox_UDDI_Key" ] || [ -z "$Infoblox_Portal" ]; then |
|||
Infoblox_UDDI_Key="" |
|||
Infoblox_Portal="" |
|||
_err "You didn't specify the Infoblox UDDI key or server (Infoblox_UDDI_Key; Infoblox_Portal)." |
|||
_err "Please set them via EXPORT Infoblox_UDDI_Key=your_key, EXPORT Infoblox_Portal=csp.infoblox.com and try again." |
|||
return 1 |
|||
fi |
|||
|
|||
_saveaccountconf_mutable Infoblox_UDDI_Key "$Infoblox_UDDI_Key" |
|||
_saveaccountconf_mutable Infoblox_Portal "$Infoblox_Portal" |
|||
|
|||
export _H1="Authorization: Token $Infoblox_UDDI_Key" |
|||
export _H2="Content-Type: application/json" |
|||
|
|||
_debug "First detect the root zone" |
|||
if ! _get_root "$fulldomain"; then |
|||
_err "invalid domain" |
|||
return 1 |
|||
fi |
|||
_debug _domain_id "$_domain_id" |
|||
_debug _sub_domain "$_sub_domain" |
|||
_debug _domain "$_domain" |
|||
|
|||
_debug "Getting existing txt records" |
|||
_infoblox_rest GET "dns/record?_filter=type%20eq%20'TXT'%20and%20name_in_zone%20eq%20'$_sub_domain'%20and%20zone%20eq%20'$_domain_id'" |
|||
|
|||
_info "Adding record" |
|||
body="{\"type\":\"TXT\",\"name_in_zone\":\"$_sub_domain\",\"zone\":\"$_domain_id\",\"ttl\":120,\"inheritance_sources\":{\"ttl\":{\"action\":\"override\"}},\"rdata\":{\"text\":\"$txtvalue\"}}" |
|||
|
|||
if _infoblox_rest POST "dns/record" "$body"; then |
|||
if _contains "$response" "$txtvalue"; then |
|||
_info "Added, OK" |
|||
return 0 |
|||
elif _contains "$response" '"error"'; then |
|||
# Check if record already exists |
|||
if _contains "$response" "already exists" || _contains "$response" "duplicate"; then |
|||
_info "Already exists, OK" |
|||
return 0 |
|||
else |
|||
_err "Add txt record error." |
|||
_err "Response: $response" |
|||
return 1 |
|||
fi |
|||
else |
|||
_info "Added, OK" |
|||
return 0 |
|||
fi |
|||
fi |
|||
_err "Add txt record error." |
|||
return 1 |
|||
} |
|||
|
|||
#Usage: dns_infoblox_uddi_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" |
|||
dns_infoblox_uddi_rm() { |
|||
fulldomain=$1 |
|||
txtvalue=$2 |
|||
|
|||
Infoblox_UDDI_Key="${Infoblox_UDDI_Key:-$(_readaccountconf_mutable Infoblox_UDDI_Key)}" |
|||
Infoblox_Portal="${Infoblox_Portal:-$(_readaccountconf_mutable Infoblox_Portal)}" |
|||
|
|||
if [ -z "$Infoblox_UDDI_Key" ] || [ -z "$Infoblox_Portal" ]; then |
|||
_err "Credentials not found" |
|||
return 1 |
|||
fi |
|||
|
|||
_info "Using Infoblox UDDI API" |
|||
_debug fulldomain "$fulldomain" |
|||
_debug txtvalue "$txtvalue" |
|||
|
|||
export _H1="Authorization: Token $Infoblox_UDDI_Key" |
|||
export _H2="Content-Type: application/json" |
|||
|
|||
_debug "First detect the root zone" |
|||
if ! _get_root "$fulldomain"; then |
|||
_err "invalid domain" |
|||
return 1 |
|||
fi |
|||
_debug _domain_id "$_domain_id" |
|||
_debug _sub_domain "$_sub_domain" |
|||
_debug _domain "$_domain" |
|||
|
|||
_debug "Getting txt records to delete" |
|||
# Filter by txtvalue to support wildcard certs (multiple TXT records) |
|||
filter="type%20eq%20'TXT'%20and%20name_in_zone%20eq%20'$_sub_domain'%20and%20zone%20eq%20'$_domain_id'%20and%20rdata.text%20eq%20'$txtvalue'" |
|||
_infoblox_rest GET "dns/record?_filter=$filter" |
|||
|
|||
if ! _contains "$response" '"results"'; then |
|||
_info "Don't need to remove, record not found." |
|||
return 0 |
|||
fi |
|||
|
|||
record_id=$(echo "$response" | _egrep_o '"id":[[:space:]]*"[^"]*"' | _head_n 1 | cut -d '"' -f 4) |
|||
_debug "record_id" "$record_id" |
|||
|
|||
if [ -z "$record_id" ]; then |
|||
_info "Don't need to remove, record not found." |
|||
return 0 |
|||
fi |
|||
|
|||
# Extract UUID from the full record ID (format: dns/record/uuid) |
|||
record_uuid=$(echo "$record_id" | sed 's|.*/||') |
|||
_debug "record_uuid" "$record_uuid" |
|||
|
|||
if ! _infoblox_rest DELETE "dns/record/$record_uuid"; then |
|||
_err "Delete record error." |
|||
return 1 |
|||
fi |
|||
|
|||
_info "Removed record successfully" |
|||
return 0 |
|||
} |
|||
|
|||
#################### Private functions below ################################## |
|||
|
|||
#_acme-challenge.www.domain.com |
|||
#returns |
|||
# _sub_domain=_acme-challenge.www |
|||
# _domain=domain.com |
|||
# _domain_id=dns/auth_zone/xxxx-xxxx |
|||
_get_root() { |
|||
domain=$1 |
|||
i=1 |
|||
p=1 |
|||
|
|||
# Remove _acme-challenge prefix if present |
|||
domain_no_acme=$(echo "$domain" | sed 's/^_acme-challenge\.//') |
|||
|
|||
while true; do |
|||
h=$(printf "%s" "$domain_no_acme" | cut -d . -f "$i"-100) |
|||
_debug h "$h" |
|||
if [ -z "$h" ]; then |
|||
# not valid |
|||
return 1 |
|||
fi |
|||
|
|||
# Query for the zone with both trailing dot and without |
|||
filter="fqdn%20eq%20'$h.'%20or%20fqdn%20eq%20'$h'" |
|||
if ! _infoblox_rest GET "dns/auth_zone?_filter=$filter"; then |
|||
# API error - don't continue if we get auth errors |
|||
if _contains "$response" "401" || _contains "$response" "Authorization"; then |
|||
_err "Authentication failed. Please check your Infoblox_UDDI_Key." |
|||
return 1 |
|||
fi |
|||
# For other errors, continue to parent domain |
|||
p=$i |
|||
i=$((i + 1)) |
|||
continue |
|||
fi |
|||
|
|||
# Check if response contains results (even if empty) |
|||
if _contains "$response" '"results"'; then |
|||
# Extract zone ID - must match the pattern dns/auth_zone/... |
|||
zone_id=$(echo "$response" | _egrep_o '"id":[[:space:]]*"dns/auth_zone/[^"]*"' | _head_n 1 | cut -d '"' -f 4) |
|||
if [ -n "$zone_id" ]; then |
|||
# Found the zone |
|||
_domain="$h" |
|||
_domain_id="$zone_id" |
|||
|
|||
# Calculate subdomain |
|||
if [ "$_domain" = "$domain" ]; then |
|||
_sub_domain="" |
|||
else |
|||
_cutlength=$((${#domain} - ${#_domain} - 1)) |
|||
_sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cutlength") |
|||
fi |
|||
|
|||
return 0 |
|||
fi |
|||
fi |
|||
|
|||
p=$i |
|||
i=$((i + 1)) |
|||
done |
|||
|
|||
return 1 |
|||
} |
|||
|
|||
# _infoblox_rest GET "dns/record?_filter=..." |
|||
# _infoblox_rest POST "dns/record" "{json body}" |
|||
# _infoblox_rest DELETE "dns/record/uuid" |
|||
_infoblox_rest() { |
|||
method=$1 |
|||
ep="$2" |
|||
data="$3" |
|||
|
|||
_debug "$ep" |
|||
|
|||
# Ensure credentials are available (when called from _get_root) |
|||
Infoblox_UDDI_Key="${Infoblox_UDDI_Key:-$(_readaccountconf_mutable Infoblox_UDDI_Key)}" |
|||
Infoblox_Portal="${Infoblox_Portal:-$(_readaccountconf_mutable Infoblox_Portal)}" |
|||
|
|||
Infoblox_UDDI_Api="https://$Infoblox_Portal/api/ddi/v1" |
|||
export _H1="Authorization: Token $Infoblox_UDDI_Key" |
|||
export _H2="Content-Type: application/json" |
|||
|
|||
# Debug (masked) |
|||
_tok_len=$(printf "%s" "$Infoblox_UDDI_Key" | wc -c | tr -d ' \n') |
|||
_debug2 "Auth header set" "Token len=${_tok_len} on $Infoblox_Portal" |
|||
|
|||
if [ "$method" != "GET" ]; then |
|||
_debug data "$data" |
|||
response="$(_post "$data" "$Infoblox_UDDI_Api/$ep" "" "$method")" |
|||
else |
|||
response="$(_get "$Infoblox_UDDI_Api/$ep")" |
|||
fi |
|||
|
|||
_ret="$?" |
|||
_debug2 response "$response" |
|||
|
|||
if [ "$_ret" != "0" ]; then |
|||
_err "Error: $ep" |
|||
return 1 |
|||
fi |
|||
|
|||
return 0 |
|||
} |
|||
@ -0,0 +1,109 @@ |
|||
#!/usr/bin/env sh |
|||
# shellcheck disable=SC2034 |
|||
dns_mgwm_info='mgw-media.de |
|||
Site: mgw-media.de |
|||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_mgwm |
|||
Options: |
|||
MGWM_CUSTOMER Your customer number |
|||
MGWM_API_HASH Your API Hash |
|||
Issues: github.com/acmesh-official/acme.sh/issues/6669 |
|||
' |
|||
# Base URL for the mgw-media.de API |
|||
MGWM_API_BASE="https://api.mgw-media.de/record" |
|||
|
|||
######## Public functions ##################### |
|||
|
|||
# This function is called by acme.sh to add a TXT record. |
|||
dns_mgwm_add() { |
|||
fulldomain=$1 |
|||
txtvalue=$2 |
|||
_info "Using mgw-media.de DNS API for domain $fulldomain (add record)" |
|||
_debug "fulldomain: $fulldomain" |
|||
_debug "txtvalue: $txtvalue" |
|||
|
|||
# Call the new private function to handle the API request. |
|||
# The 'add' action, fulldomain, type 'txt' and txtvalue are passed. |
|||
if _mgwm_request "add" "$fulldomain" "txt" "$txtvalue"; then |
|||
_info "TXT record for $fulldomain successfully added via mgw-media.de API." |
|||
_sleep 10 # Wait briefly for DNS propagation, a common practice in DNS-01 hooks. |
|||
return 0 |
|||
else |
|||
# Error message already logged by _mgwm_request, but a specific one here helps. |
|||
_err "mgwm_add: Failed to add TXT record for $fulldomain." |
|||
return 1 |
|||
fi |
|||
} |
|||
# This function is called by acme.sh to remove a TXT record after validation. |
|||
dns_mgwm_rm() { |
|||
fulldomain=$1 |
|||
txtvalue=$2 # This txtvalue is now used to identify the specific record to be removed. |
|||
_info "Removing TXT record for $fulldomain using mgw-media.de DNS API (remove record)" |
|||
_debug "fulldomain: $fulldomain" |
|||
_debug "txtvalue: $txtvalue" |
|||
|
|||
# Call the new private function to handle the API request. |
|||
# The 'rm' action, fulldomain, type 'txt' and txtvalue are passed. |
|||
if _mgwm_request "rm" "$fulldomain" "txt" "$txtvalue"; then |
|||
_info "TXT record for $fulldomain successfully removed via mgw-media.de API." |
|||
return 0 |
|||
else |
|||
# Error message already logged by _mgwm_request, but a specific one here helps. |
|||
_err "mgwm_rm: Failed to remove TXT record for $fulldomain." |
|||
return 1 |
|||
fi |
|||
} |
|||
#################### Private functions below ################################## |
|||
|
|||
# _mgwm_request() encapsulates the API call logic, including |
|||
# loading credentials, setting the Authorization header, and executing the request. |
|||
# Arguments: |
|||
# $1: action (e.g., "add", "rm") |
|||
# $2: fulldomain |
|||
# $3: type (e.g., "txt") |
|||
# $4: content (the txtvalue) |
|||
_mgwm_request() { |
|||
_action="$1" |
|||
_fulldomain="$2" |
|||
_type="$3" |
|||
_content="$4" |
|||
|
|||
_debug "Calling _mgwm_request for action: $_action, domain: $_fulldomain, type: $_type, content: $_content" |
|||
|
|||
# Load credentials from environment or acme.sh config |
|||
MGWM_CUSTOMER="${MGWM_CUSTOMER:-$(_readaccountconf_mutable MGWM_CUSTOMER)}" |
|||
MGWM_API_HASH="${MGWM_API_HASH:-$(_readaccountconf_mutable MGWM_API_HASH)}" |
|||
|
|||
# Check if credentials are set |
|||
if [ -z "$MGWM_CUSTOMER" ] || [ -z "$MGWM_API_HASH" ]; then |
|||
_err "You didn't specify one or more of MGWM_CUSTOMER or MGWM_API_HASH." |
|||
_err "Please check these environment variables and try again." |
|||
return 1 |
|||
fi |
|||
|
|||
# Save credentials for automatic renewal and future calls |
|||
_saveaccountconf_mutable MGWM_CUSTOMER "$MGWM_CUSTOMER" |
|||
_saveaccountconf_mutable MGWM_API_HASH "$MGWM_API_HASH" |
|||
|
|||
# Create the Basic Auth Header. acme.sh's _base64 function is used for encoding. |
|||
_credentials="$(printf "%s:%s" "$MGWM_CUSTOMER" "$MGWM_API_HASH" | _base64)" |
|||
export _H1="Authorization: Basic $_credentials" |
|||
_debug "Set Authorization Header: Basic <credentials_encoded>" # Log debug message without sensitive credentials |
|||
|
|||
# Construct the API URL based on the action and provided parameters. |
|||
_request_url="${MGWM_API_BASE}/${_action}/${_fulldomain}/${_type}/${_content}" |
|||
_debug "Constructed mgw-media.de API URL for action '$_action': ${_request_url}" |
|||
|
|||
# Execute the HTTP GET request with the Authorization Header. |
|||
# The 5th parameter of _get is where acme.sh expects custom HTTP headers like Authorization. |
|||
response="$(_get "$_request_url")" |
|||
_debug "mgw-media.de API response for action '$_action': $response" |
|||
|
|||
# Check the API response for success. The API returns "OK" on success. |
|||
if [ "$response" = "OK" ]; then |
|||
_info "mgw-media.de API action '$_action' for record '$_fulldomain' successful." |
|||
return 0 |
|||
else |
|||
_err "Failed mgw-media.de API action '$_action' for record '$_fulldomain'. Unexpected API Response: '$response'" |
|||
return 1 |
|||
fi |
|||
} |
|||
@ -0,0 +1,214 @@ |
|||
#!/usr/bin/env sh |
|||
# shellcheck disable=SC2034 |
|||
dns_mijnhost_info='mijn.host |
|||
Site: mijn.host |
|||
Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_mijnhost |
|||
Options: |
|||
MIJNHOST_API_KEY API Key |
|||
Issues: github.com/acmesh-official/acme.sh/issues/6177 |
|||
Author: @peterv99 |
|||
' |
|||
|
|||
######## Public functions ###################### |
|||
MIJNHOST_API="https://mijn.host/api/v2" |
|||
|
|||
# Add TXT record for domain verification |
|||
dns_mijnhost_add() { |
|||
fulldomain=$1 |
|||
txtvalue=$2 |
|||
|
|||
MIJNHOST_API_KEY="${MIJNHOST_API_KEY:-$(_readaccountconf_mutable MIJNHOST_API_KEY)}" |
|||
if [ -z "$MIJNHOST_API_KEY" ]; then |
|||
MIJNHOST_API_KEY="" |
|||
_err "You haven't specified your mijn-host API key yet." |
|||
_err "Please add MIJNHOST_API_KEY to the env." |
|||
return 1 |
|||
fi |
|||
|
|||
# Save the API key for future use |
|||
_saveaccountconf_mutable MIJNHOST_API_KEY "$MIJNHOST_API_KEY" |
|||
|
|||
_debug "First detect the root zone" |
|||
if ! _get_root "$fulldomain"; then |
|||
_err "Invalid domain" |
|||
return 1 |
|||
fi |
|||
|
|||
_debug2 _sub_domain "$_sub_domain" |
|||
_debug2 _domain "$_domain" |
|||
_debug "Adding DNS record" "${fulldomain}." |
|||
|
|||
# Construct the API URL |
|||
api_url="$MIJNHOST_API/domains/$_domain/dns" |
|||
|
|||
# Getting previous records |
|||
_mijnhost_rest GET "$api_url" "" |
|||
|
|||
if [ "$_code" != "200" ]; then |
|||
_err "Error getting current DNS enties ($_code)" |
|||
return 1 |
|||
fi |
|||
|
|||
records=$(echo "$response" | _egrep_o '"records":\[.*\]' | sed 's/"records"://') |
|||
|
|||
_debug2 "Current records" "$records" |
|||
|
|||
# Build the payload for the API |
|||
data="{\"type\":\"TXT\",\"name\":\"$fulldomain.\",\"value\":\"$txtvalue\",\"ttl\":300}" |
|||
|
|||
_debug2 "Record to add" "$data" |
|||
|
|||
# Updating the records |
|||
updated_records=$(echo "$records" | sed -E "s/\]( *$)/,$data\]/") |
|||
|
|||
_debug2 "Updated records" "$updated_records" |
|||
|
|||
# data |
|||
data="{\"records\": $updated_records}" |
|||
|
|||
_mijnhost_rest PUT "$api_url" "$data" |
|||
|
|||
if [ "$_code" = "200" ]; then |
|||
_info "DNS record succesfully added." |
|||
return 0 |
|||
else |
|||
_err "Error adding DNS record ($_code)." |
|||
return 1 |
|||
fi |
|||
} |
|||
|
|||
# Remove TXT record after verification |
|||
dns_mijnhost_rm() { |
|||
fulldomain=$1 |
|||
txtvalue=$2 |
|||
|
|||
MIJNHOST_API_KEY="${MIJNHOST_API_KEY:-$(_readaccountconf_mutable MIJNHOST_API_KEY)}" |
|||
if [ -z "$MIJNHOST_API_KEY" ]; then |
|||
MIJNHOST_API_KEY="" |
|||
_err "You haven't specified your mijn-host API key yet." |
|||
_err "Please add MIJNHOST_API_KEY to the env." |
|||
return 1 |
|||
fi |
|||
|
|||
_debug "Detecting root zone for" "${fulldomain}." |
|||
if ! _get_root "$fulldomain"; then |
|||
_err "Invalid domain" |
|||
return 1 |
|||
fi |
|||
|
|||
_debug "Removing DNS record for TXT value" "${txtvalue}." |
|||
|
|||
# Construct the API URL |
|||
api_url="$MIJNHOST_API/domains/$_domain/dns" |
|||
|
|||
# Get current records |
|||
_mijnhost_rest GET "$api_url" "" |
|||
|
|||
if [ "$_code" != "200" ]; then |
|||
_err "Error getting current DNS enties ($_code)" |
|||
return 1 |
|||
fi |
|||
|
|||
_debug2 "Get current records response:" "$response" |
|||
|
|||
records=$(echo "$response" | _egrep_o '"records":\[.*\]' | sed 's/"records"://') |
|||
|
|||
_debug2 "Current records:" "$records" |
|||
|
|||
updated_records=$(echo "$records" | sed -E "s/\{[^}]*\"value\":\"$txtvalue\"[^}]*\},?//g" | sed 's/,]/]/g') |
|||
|
|||
_debug2 "Updated records:" "$updated_records" |
|||
|
|||
# Build the new payload |
|||
data="{\"records\": $updated_records}" |
|||
|
|||
# Use the _put method to update the records |
|||
_mijnhost_rest PUT "$api_url" "$data" |
|||
|
|||
if [ "$_code" = "200" ]; then |
|||
_info "DNS record removed successfully." |
|||
return 0 |
|||
else |
|||
_err "Error removing DNS record ($_code)." |
|||
return 1 |
|||
fi |
|||
} |
|||
|
|||
# Helper function to detect the root zone |
|||
_get_root() { |
|||
domain=$1 |
|||
|
|||
# Get current records |
|||
_debug "Getting current domains" |
|||
_mijnhost_rest GET "$MIJNHOST_API/domains" "" |
|||
|
|||
if [ "$_code" != "200" ]; then |
|||
_err "error getting current domains ($_code)" |
|||
return 1 |
|||
fi |
|||
|
|||
# Extract root domains from response |
|||
rootDomains=$(echo "$response" | _egrep_o '"domain":"[^"]*"' | sed -E 's/"domain":"([^"]*)"/\1/') |
|||
_debug "Root domains:" "$rootDomains" |
|||
|
|||
for rootDomain in $rootDomains; do |
|||
if _contains "$domain" "$rootDomain"; then |
|||
_domain="$rootDomain" |
|||
_sub_domain=$(echo "$domain" | sed "s/.$rootDomain//g") |
|||
_debug "Found root domain" "$_domain" "and subdomain" "$_sub_domain" "for" "$domain" |
|||
return 0 |
|||
fi |
|||
done |
|||
return 1 |
|||
} |
|||
|
|||
# Helper function for rest calls |
|||
_mijnhost_rest() { |
|||
m=$1 |
|||
ep="$2" |
|||
data="$3" |
|||
|
|||
MAX_REQUEST_RETRY_TIMES=15 |
|||
_request_retry_times=0 |
|||
_retry_sleep=5 #Initial sleep time in seconds. |
|||
|
|||
while [ "${_request_retry_times}" -lt "$MAX_REQUEST_RETRY_TIMES" ]; do |
|||
_debug2 _request_retry_times "$_request_retry_times" |
|||
export _H1="API-Key: $MIJNHOST_API_KEY" |
|||
export _H2="Content-Type: application/json" |
|||
# clear headers from previous request to avoid getting wrong http code on timeouts |
|||
: >"$HTTP_HEADER" |
|||
_debug "$ep" |
|||
if [ "$m" != "GET" ]; then |
|||
_debug2 "data $data" |
|||
response="$(_post "$data" "$ep" "" "$m")" |
|||
else |
|||
response="$(_get "$ep")" |
|||
fi |
|||
_ret="$?" |
|||
_debug2 "response $response" |
|||
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")" |
|||
_debug "http response code $_code" |
|||
if [ "$_code" = "401" ]; then |
|||
# we have an invalid API token, maybe it is expired? |
|||
_err "Access denied. Invalid API token." |
|||
return 1 |
|||
fi |
|||
|
|||
if [ "$_ret" != "0" ] || [ -z "$_code" ] || [ "$_code" = "400" ] || _contains "$response" "DNS records not managed by mijn.host"; then #Sometimes API errors out |
|||
_request_retry_times="$(_math "$_request_retry_times" + 1)" |
|||
_info "REST call error $_code retrying $ep in ${_retry_sleep}s" |
|||
_sleep "$_retry_sleep" |
|||
_retry_sleep="$(_math "$_retry_sleep" \* 2)" |
|||
continue |
|||
fi |
|||
break |
|||
done |
|||
if [ "$_request_retry_times" = "$MAX_REQUEST_RETRY_TIMES" ]; then |
|||
_err "Error mijn.host API call was retried $MAX_REQUEST_RETRY_TIMES times." |
|||
_err "Calling $ep failed." |
|||
return 1 |
|||
fi |
|||
response="$(echo "$response" | _normalizeJson)" |
|||
return 0 |
|||
} |
|||
@ -0,0 +1,186 @@ |
|||
#!/usr/bin/env sh |
|||
# shellcheck disable=SC2034 |
|||
dns_openprovider_rest_info='OpenProvider (REST) |
|||
Domains: OpenProvider.com |
|||
Site: OpenProvider.eu |
|||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_openprovider_rest |
|||
Options: |
|||
OPENPROVIDER_REST_USERNAME Openprovider Account Username |
|||
OPENPROVIDER_REST_PASSWORD Openprovider Account Password |
|||
Issues: github.com/acmesh-official/acme.sh/issues/6122 |
|||
Author: Lambiek12 |
|||
' |
|||
|
|||
OPENPROVIDER_API_URL="https://api.openprovider.eu/v1beta" |
|||
|
|||
######## Public functions ##################### |
|||
|
|||
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" |
|||
# Used to add txt record |
|||
dns_openprovider_rest_add() { |
|||
fulldomain=$1 |
|||
txtvalue=$2 |
|||
|
|||
_openprovider_prepare_credentials || return 1 |
|||
|
|||
_debug "Try fetch OpenProvider DNS zone details" |
|||
if ! _get_dns_zone "$fulldomain"; then |
|||
_err "DNS zone not found within configured OpenProvider account." |
|||
return 1 |
|||
fi |
|||
|
|||
if [ -n "$_domain_id" ]; then |
|||
addzonerecordrequestparameters="dns/zones/$_domain_name" |
|||
addzonerecordrequestbody="{\"id\":$_domain_id,\"name\":\"$_domain_name\",\"records\":{\"add\":[{\"name\":\"$_sub_domain\",\"ttl\":900,\"type\":\"TXT\",\"value\":\"$txtvalue\"}]}}" |
|||
|
|||
if _openprovider_rest PUT "$addzonerecordrequestparameters" "$addzonerecordrequestbody"; then |
|||
if _contains "$response" "\"success\":true"; then |
|||
return 0 |
|||
elif _contains "$response" "\"Duplicate record\""; then |
|||
_debug "Record already existed" |
|||
return 0 |
|||
else |
|||
_err "Adding TXT record failed due to errors." |
|||
return 1 |
|||
fi |
|||
fi |
|||
fi |
|||
|
|||
_err "Adding TXT record failed due to errors." |
|||
return 1 |
|||
} |
|||
|
|||
# Usage: rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" |
|||
# Used to remove the txt record after validation |
|||
dns_openprovider_rest_rm() { |
|||
fulldomain=$1 |
|||
txtvalue=$2 |
|||
|
|||
_openprovider_prepare_credentials || return 1 |
|||
|
|||
_debug "Try fetch OpenProvider DNS zone details" |
|||
if ! _get_dns_zone "$fulldomain"; then |
|||
_err "DNS zone not found within configured OpenProvider account." |
|||
return 1 |
|||
fi |
|||
|
|||
if [ -n "$_domain_id" ]; then |
|||
removezonerecordrequestparameters="dns/zones/$_domain_name" |
|||
removezonerecordrequestbody="{\"id\":$_domain_id,\"name\":\"$_domain_name\",\"records\":{\"remove\":[{\"name\":\"$_sub_domain\",\"ttl\":900,\"type\":\"TXT\",\"value\":\"\\\"$txtvalue\\\"\"}]}}" |
|||
|
|||
if _openprovider_rest PUT "$removezonerecordrequestparameters" "$removezonerecordrequestbody"; then |
|||
if _contains "$response" "\"success\":true"; then |
|||
return 0 |
|||
else |
|||
_err "Removing TXT record failed due to errors." |
|||
return 1 |
|||
fi |
|||
fi |
|||
fi |
|||
|
|||
_err "Removing TXT record failed due to errors." |
|||
return 1 |
|||
} |
|||
|
|||
#################### OpenProvider API common functions #################### |
|||
_openprovider_prepare_credentials() { |
|||
OPENPROVIDER_REST_USERNAME="${OPENPROVIDER_REST_USERNAME:-$(_readaccountconf_mutable OPENPROVIDER_REST_USERNAME)}" |
|||
OPENPROVIDER_REST_PASSWORD="${OPENPROVIDER_REST_PASSWORD:-$(_readaccountconf_mutable OPENPROVIDER_REST_PASSWORD)}" |
|||
|
|||
if [ -z "$OPENPROVIDER_REST_USERNAME" ] || [ -z "$OPENPROVIDER_REST_PASSWORD" ]; then |
|||
OPENPROVIDER_REST_USERNAME="" |
|||
OPENPROVIDER_REST_PASSWORD="" |
|||
_err "You didn't specify the Openprovider username or password yet." |
|||
return 1 |
|||
fi |
|||
|
|||
#save the credentials to the account conf file. |
|||
_saveaccountconf_mutable OPENPROVIDER_REST_USERNAME "$OPENPROVIDER_REST_USERNAME" |
|||
_saveaccountconf_mutable OPENPROVIDER_REST_PASSWORD "$OPENPROVIDER_REST_PASSWORD" |
|||
} |
|||
|
|||
_openprovider_rest() { |
|||
httpmethod=$1 |
|||
queryparameters=$2 |
|||
requestbody=$3 |
|||
|
|||
_openprovider_rest_login |
|||
if [ -z "$openproviderauthtoken" ]; then |
|||
_err "Unable to fetch authentication token from Openprovider API." |
|||
return 1 |
|||
fi |
|||
|
|||
export _H1="Content-Type: application/json" |
|||
export _H2="Accept: application/json" |
|||
export _H3="Authorization: Bearer $openproviderauthtoken" |
|||
|
|||
if [ "$httpmethod" != "GET" ]; then |
|||
response="$(_post "$requestbody" "$OPENPROVIDER_API_URL/$queryparameters" "" "$httpmethod")" |
|||
else |
|||
response="$(_get "$OPENPROVIDER_API_URL/$queryparameters")" |
|||
fi |
|||
|
|||
if [ "$?" != "0" ]; then |
|||
_err "No valid parameters supplied for Openprovider API: Error $queryparameters" |
|||
return 1 |
|||
fi |
|||
|
|||
_debug2 response "$response" |
|||
|
|||
return 0 |
|||
} |
|||
|
|||
_openprovider_rest_login() { |
|||
export _H1="Content-Type: application/json" |
|||
export _H2="Accept: application/json" |
|||
|
|||
loginrequesturl="$OPENPROVIDER_API_URL/auth/login" |
|||
loginrequestbody="{\"ip\":\"0.0.0.0\",\"password\":\"$OPENPROVIDER_REST_PASSWORD\",\"username\":\"$OPENPROVIDER_REST_USERNAME\"}" |
|||
loginresponse="$(_post "$loginrequestbody" "$loginrequesturl" "" "POST")" |
|||
|
|||
openproviderauthtoken="$(printf "%s\n" "$loginresponse" | _egrep_o '"token" *: *"[^"]*' | _head_n 1 | sed 's#^"token" *: *"##')" |
|||
|
|||
export openproviderauthtoken |
|||
} |
|||
|
|||
#################### Private functions ################################## |
|||
|
|||
# Usage: _get_dns_zone _acme-challenge.www.domain.com |
|||
# Returns: |
|||
# _domain_id=123456789 |
|||
# _domain_name=domain.com |
|||
# _sub_domain=_acme-challenge.www |
|||
_get_dns_zone() { |
|||
domain=$1 |
|||
i=1 |
|||
p=1 |
|||
|
|||
while true; do |
|||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) |
|||
if [ -z "$h" ]; then |
|||
# Empty value not allowed |
|||
return 1 |
|||
fi |
|||
|
|||
if ! _openprovider_rest GET "dns/zones/$h" ""; then |
|||
return 1 |
|||
fi |
|||
|
|||
if _contains "$response" "\"name\":\"$h\""; then |
|||
_domain_id="$(printf "%s\n" "$response" | _egrep_o '"id" *: *[^,]*' | _head_n 1 | sed 's#^"id" *: *##')" |
|||
_debug _domain_id "$_domain_id" |
|||
|
|||
_domain_name="$h" |
|||
_debug _domain_name "$_domain_name" |
|||
|
|||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") |
|||
_debug _sub_domain "$_sub_domain" |
|||
return 0 |
|||
fi |
|||
|
|||
p=$i |
|||
i=$(_math "$i" + 1) |
|||
done |
|||
|
|||
return 1 |
|||
} |
|||
@ -0,0 +1,216 @@ |
|||
#!/usr/bin/env sh |
|||
# shellcheck disable=SC2034 |
|||
dns_qc_info='QUIC.cloud |
|||
Site: quic.cloud |
|||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_qc |
|||
Options: |
|||
QC_API_KEY QC API Key |
|||
QC_API_EMAIL Your account email |
|||
' |
|||
|
|||
QC_Api="https://api.quic.cloud/v2" |
|||
|
|||
######## Public functions ##################### |
|||
|
|||
#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" |
|||
dns_qc_add() { |
|||
fulldomain=$1 |
|||
txtvalue=$2 |
|||
|
|||
_debug "Enter dns_qc_add fulldomain: $fulldomain, txtvalue: $txtvalue" |
|||
QC_API_KEY="${QC_API_KEY:-$(_readaccountconf_mutable QC_API_KEY)}" |
|||
QC_API_EMAIL="${QC_API_EMAIL:-$(_readaccountconf_mutable QC_API_EMAIL)}" |
|||
|
|||
if [ "$QC_API_KEY" ]; then |
|||
_saveaccountconf_mutable QC_API_KEY "$QC_API_KEY" |
|||
else |
|||
_err "You didn't specify a QUIC.cloud api key as QC_API_KEY." |
|||
_err "You can get yours from here https://my.quic.cloud/up/api." |
|||
return 1 |
|||
fi |
|||
|
|||
if ! _contains "$QC_API_EMAIL" "@"; then |
|||
_err "It seems that the QC_API_EMAIL=$QC_API_EMAIL is not a valid email address." |
|||
_err "Please check and retry." |
|||
return 1 |
|||
fi |
|||
#save the api key and email to the account conf file. |
|||
_saveaccountconf_mutable QC_API_EMAIL "$QC_API_EMAIL" |
|||
|
|||
_debug "First detect the root zone" |
|||
if ! _get_root "$fulldomain"; then |
|||
_err "invalid domain during add" |
|||
return 1 |
|||
fi |
|||
_debug _domain_id "$_domain_id" |
|||
_debug _sub_domain "$_sub_domain" |
|||
_debug _domain "$_domain" |
|||
|
|||
_debug "Getting txt records" |
|||
_qc_rest GET "zones/${_domain_id}/records" |
|||
|
|||
if ! echo "$response" | tr -d " " | grep \"success\":true >/dev/null; then |
|||
_err "Error failed response from QC GET: $response" |
|||
return 1 |
|||
fi |
|||
|
|||
# For wildcard cert, the main root domain and the wildcard domain have the same txt subdomain name, so |
|||
# we can not use updating anymore. |
|||
# count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2) |
|||
# _debug count "$count" |
|||
# if [ "$count" = "0" ]; then |
|||
_info "Adding txt record" |
|||
if _qc_rest POST "zones/$_domain_id/records" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":1800}"; then |
|||
if _contains "$response" "$txtvalue"; then |
|||
_info "Added txt record, OK" |
|||
return 0 |
|||
elif _contains "$response" "Same record already exists"; then |
|||
_info "txt record already exists, OK" |
|||
return 0 |
|||
else |
|||
_err "Add txt record error: $response" |
|||
return 1 |
|||
fi |
|||
fi |
|||
_err "Add txt record error: POST failed: $response" |
|||
return 1 |
|||
|
|||
} |
|||
|
|||
#fulldomain txtvalue |
|||
dns_qc_rm() { |
|||
fulldomain=$1 |
|||
txtvalue=$2 |
|||
|
|||
_debug "Enter dns_qc_rm fulldomain: $fulldomain, txtvalue: $txtvalue" |
|||
QC_API_KEY="${QC_API_KEY:-$(_readaccountconf_mutable QC_API_KEY)}" |
|||
QC_API_EMAIL="${QC_API_EMAIL:-$(_readaccountconf_mutable QC_API_EMAIL)}" |
|||
|
|||
_debug "First detect the root zone" |
|||
if ! _get_root "$fulldomain"; then |
|||
_err "invalid domain during rm" |
|||
return 1 |
|||
fi |
|||
_debug _domain_id "$_domain_id" |
|||
_debug _sub_domain "$_sub_domain" |
|||
_debug _domain "$_domain" |
|||
|
|||
_debug "Getting txt records" |
|||
_qc_rest GET "zones/${_domain_id}/records" |
|||
|
|||
if ! echo "$response" | tr -d " " | grep \"success\":true >/dev/null; then |
|||
_err "Error rm GET response: $response" |
|||
return 1 |
|||
fi |
|||
|
|||
_debug "Pre-jq response:" "$response" |
|||
# Do not use jq or subsequent code |
|||
#response=$(echo "$response" | jq ".result[] | select(.id) | select(.content == \"$txtvalue\") | select(.type == \"TXT\")") |
|||
#_debug "get txt response" "$response" |
|||
#if [ "${response}" = "" ]; then |
|||
# _info "Don't need to remove txt records." |
|||
# return 0 |
|||
#fi |
|||
#record_id=$(echo "$response" | grep \"id\" | awk -F ' ' '{print $2}' | sed 's/,$//') |
|||
#_debug "txt record_id" "$record_id" |
|||
#Instead of jq |
|||
array=$(echo "$response" | grep -o '\[[^]]*\]' | sed 's/^\[\(.*\)\]$/\1/') |
|||
if [ -z "$array" ]; then |
|||
_err "Expected array in QC response: $response" |
|||
return 1 |
|||
fi |
|||
# Temporary file to hold matched content (one per line) |
|||
tmpfile=$(_mktemp) |
|||
echo "$array" | grep -o '{[^}]*}' | sed 's/^{//;s/}$//' >"$tmpfile" |
|||
record_id="" |
|||
|
|||
while IFS= read -r obj || [ -n "$obj" ]; do |
|||
if echo "$obj" | grep -q '"TXT"' && echo "$obj" | grep -q '"id"' && echo "$obj" | grep -q "$txtvalue"; then |
|||
_debug "response includes" "$obj" |
|||
record_id=$(echo "$obj" | sed 's/^\"id\":\([0-9]\+\).*/\1/') |
|||
break |
|||
fi |
|||
done <"$tmpfile" |
|||
|
|||
rm "$tmpfile" |
|||
|
|||
if [ -z "$record_id" ]; then |
|||
_info "TXT record, or $txtvalue not found, nothing to remove" |
|||
return 0 |
|||
fi |
|||
|
|||
#End of jq replacement |
|||
if ! _qc_rest DELETE "zones/$_domain_id/records/$record_id"; then |
|||
_info "Delete txt record error." |
|||
return 1 |
|||
fi |
|||
|
|||
_info "TXT Record ID: $record_id successfully deleted" |
|||
return 0 |
|||
|
|||
} |
|||
|
|||
#################### Private functions below ################################## |
|||
#_acme-challenge.www.domain.com |
|||
#returns |
|||
# _sub_domain=_acme-challenge.www |
|||
# _domain=domain.com |
|||
# _domain_id=sdjkglgdfewsdfg |
|||
_get_root() { |
|||
domain=$1 |
|||
i=1 |
|||
p=1 |
|||
|
|||
h=$(printf "%s" "$domain" | cut -d . -f2-) |
|||
_debug h "$h" |
|||
if [ -z "$h" ]; then |
|||
_err "$h ($domain) is an invalid domain" |
|||
return 1 |
|||
fi |
|||
|
|||
if ! _qc_rest GET "zones"; then |
|||
_err "qc_rest failed" |
|||
return 1 |
|||
fi |
|||
|
|||
if _contains "$response" "\"name\":\"$h\"" || _contains "$response" "\"name\":\"$h.\""; then |
|||
_domain_id=$h |
|||
if [ "$_domain_id" ]; then |
|||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") |
|||
_domain=$h |
|||
return 0 |
|||
fi |
|||
_err "Empty domain_id $h" |
|||
return 1 |
|||
fi |
|||
_err "Missing domain_id $h" |
|||
return 1 |
|||
} |
|||
|
|||
_qc_rest() { |
|||
m=$1 |
|||
ep="$2" |
|||
data="$3" |
|||
_debug "$ep" |
|||
|
|||
email_trimmed=$(echo "$QC_API_EMAIL" | tr -d '"') |
|||
token_trimmed=$(echo "$QC_API_KEY" | tr -d '"') |
|||
|
|||
export _H1="Content-Type: application/json" |
|||
export _H2="X-Auth-Email: $email_trimmed" |
|||
export _H3="X-Auth-Key: $token_trimmed" |
|||
|
|||
if [ "$m" != "GET" ]; then |
|||
_debug data "$data" |
|||
response="$(_post "$data" "$QC_Api/$ep" "" "$m")" |
|||
else |
|||
response="$(_get "$QC_Api/$ep")" |
|||
fi |
|||
|
|||
if [ "$?" != "0" ]; then |
|||
_err "error $ep" |
|||
return 1 |
|||
fi |
|||
_debug2 response "$response" |
|||
return 0 |
|||
} |
|||
@ -0,0 +1,309 @@ |
|||
#!/usr/bin/env sh |
|||
# shellcheck disable=SC2034 |
|||
dns_sotoon_info='Sotoon.ir |
|||
Site: Sotoon.ir |
|||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_sotoon |
|||
Options: |
|||
Sotoon_Token API Token |
|||
Sotoon_WorkspaceUUID Workspace UUID |
|||
Issues: github.com/acmesh-official/acme.sh/issues/6656 |
|||
Author: Erfan Gholizade |
|||
' |
|||
|
|||
SOTOON_API_URL="https://api.sotoon.ir/delivery/v2.1/global" |
|||
|
|||
######## Public functions ##################### |
|||
|
|||
#Adding the txt record for validation. |
|||
#Usage: dns_sotoon_add fulldomain TXT_record |
|||
#Usage: dns_sotoon_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" |
|||
dns_sotoon_add() { |
|||
fulldomain=$1 |
|||
txtvalue=$2 |
|||
_info_sotoon "Using Sotoon" |
|||
|
|||
Sotoon_Token="${Sotoon_Token:-$(_readaccountconf_mutable Sotoon_Token)}" |
|||
Sotoon_WorkspaceUUID="${Sotoon_WorkspaceUUID:-$(_readaccountconf_mutable Sotoon_WorkspaceUUID)}" |
|||
|
|||
if [ -z "$Sotoon_Token" ]; then |
|||
_err_sotoon "You didn't specify \"Sotoon_Token\" token yet." |
|||
_err_sotoon "You can get yours from here https://ocean.sotoon.ir/profile/tokens" |
|||
return 1 |
|||
fi |
|||
if [ -z "$Sotoon_WorkspaceUUID" ]; then |
|||
_err_sotoon "You didn't specify \"Sotoon_WorkspaceUUID\" Workspace UUID yet." |
|||
_err_sotoon "You can get yours from here https://ocean.sotoon.ir/profile/workspaces" |
|||
return 1 |
|||
fi |
|||
|
|||
#save the info to the account conf file. |
|||
_saveaccountconf_mutable Sotoon_Token "$Sotoon_Token" |
|||
_saveaccountconf_mutable Sotoon_WorkspaceUUID "$Sotoon_WorkspaceUUID" |
|||
|
|||
_debug_sotoon "First detect the root zone" |
|||
if ! _get_root "$fulldomain"; then |
|||
_err_sotoon "invalid domain" |
|||
return 1 |
|||
fi |
|||
|
|||
_info_sotoon "Adding record" |
|||
|
|||
_debug_sotoon _domain_id "$_domain_id" |
|||
_debug_sotoon _sub_domain "$_sub_domain" |
|||
_debug_sotoon _domain "$_domain" |
|||
|
|||
# First, GET the current domain zone to check for existing TXT records |
|||
# This is needed for wildcard certs which require multiple TXT values |
|||
_info_sotoon "Checking for existing TXT records" |
|||
if ! _sotoon_rest GET "$_domain_id"; then |
|||
_err_sotoon "Failed to get domain zone" |
|||
return 1 |
|||
fi |
|||
|
|||
# Check if there are existing TXT records for this subdomain |
|||
_existing_txt="" |
|||
if _contains "$response" "\"$_sub_domain\""; then |
|||
_debug_sotoon "Found existing records for $_sub_domain" |
|||
# Extract existing TXT values from the response |
|||
# The format is: "_acme-challenge":[{"TXT":"value1","type":"TXT","ttl":10},{"TXT":"value2",...}] |
|||
_existing_txt=$(echo "$response" | _egrep_o "\"$_sub_domain\":\[[^]]*\]" | sed "s/\"$_sub_domain\"://") |
|||
_debug_sotoon "Existing TXT records: $_existing_txt" |
|||
fi |
|||
|
|||
# Build the new record entry |
|||
_new_record="{\"TXT\":\"$txtvalue\",\"type\":\"TXT\",\"ttl\":120}" |
|||
|
|||
# If there are existing records, append to them; otherwise create new array |
|||
if [ -n "$_existing_txt" ] && [ "$_existing_txt" != "[]" ] && [ "$_existing_txt" != "null" ]; then |
|||
# Check if this exact TXT value already exists (avoid duplicates) |
|||
if _contains "$_existing_txt" "\"$txtvalue\""; then |
|||
_info_sotoon "TXT record already exists, skipping" |
|||
return 0 |
|||
fi |
|||
# Remove the closing bracket and append new record |
|||
_combined_records="$(echo "$_existing_txt" | sed 's/]$//'),$_new_record]" |
|||
_debug_sotoon "Combined records: $_combined_records" |
|||
else |
|||
# No existing records, create new array |
|||
_combined_records="[$_new_record]" |
|||
fi |
|||
|
|||
# Prepare the DNS record data in Kubernetes CRD format |
|||
_dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":$_combined_records}}}" |
|||
|
|||
_debug_sotoon "DNS record payload: $_dns_record" |
|||
|
|||
# Use PATCH to update/add the record to the domain zone |
|||
_info_sotoon "Updating domain zone $_domain_id with TXT record" |
|||
if _sotoon_rest PATCH "$_domain_id" "$_dns_record"; then |
|||
if _contains "$response" "$txtvalue" || _contains "$response" "\"$_sub_domain\""; then |
|||
_info_sotoon "Added, OK" |
|||
return 0 |
|||
else |
|||
_debug_sotoon "Response: $response" |
|||
_err_sotoon "Add txt record error." |
|||
return 1 |
|||
fi |
|||
fi |
|||
|
|||
_err_sotoon "Add txt record error." |
|||
return 1 |
|||
} |
|||
|
|||
#Remove the txt record after validation. |
|||
#Usage: dns_sotoon_rm fulldomain TXT_record |
|||
#Usage: dns_sotoon_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" |
|||
dns_sotoon_rm() { |
|||
fulldomain=$1 |
|||
txtvalue=$2 |
|||
_info_sotoon "Using Sotoon" |
|||
_debug_sotoon fulldomain "$fulldomain" |
|||
_debug_sotoon txtvalue "$txtvalue" |
|||
|
|||
Sotoon_Token="${Sotoon_Token:-$(_readaccountconf_mutable Sotoon_Token)}" |
|||
Sotoon_WorkspaceUUID="${Sotoon_WorkspaceUUID:-$(_readaccountconf_mutable Sotoon_WorkspaceUUID)}" |
|||
|
|||
_debug_sotoon "First detect the root zone" |
|||
if ! _get_root "$fulldomain"; then |
|||
_err_sotoon "invalid domain" |
|||
return 1 |
|||
fi |
|||
_debug_sotoon _domain_id "$_domain_id" |
|||
_debug_sotoon _sub_domain "$_sub_domain" |
|||
_debug_sotoon _domain "$_domain" |
|||
|
|||
_info_sotoon "Removing TXT record" |
|||
|
|||
# First, GET the current domain zone to check for existing TXT records |
|||
if ! _sotoon_rest GET "$_domain_id"; then |
|||
_err_sotoon "Failed to get domain zone" |
|||
return 1 |
|||
fi |
|||
|
|||
# Check if there are existing TXT records for this subdomain |
|||
_existing_txt="" |
|||
if _contains "$response" "\"$_sub_domain\""; then |
|||
_debug_sotoon "Found existing records for $_sub_domain" |
|||
_existing_txt=$(echo "$response" | _egrep_o "\"$_sub_domain\":\[[^]]*\]" | sed "s/\"$_sub_domain\"://") |
|||
_debug_sotoon "Existing TXT records: $_existing_txt" |
|||
fi |
|||
|
|||
# If no existing records, nothing to remove |
|||
if [ -z "$_existing_txt" ] || [ "$_existing_txt" = "[]" ] || [ "$_existing_txt" = "null" ]; then |
|||
_info_sotoon "No TXT records found, nothing to remove" |
|||
return 0 |
|||
fi |
|||
|
|||
# Remove the specific TXT value from the array |
|||
# This handles the case where there are multiple TXT values (wildcard certs) |
|||
_remaining_records=$(echo "$_existing_txt" | sed "s/{\"TXT\":\"$txtvalue\"[^}]*},*//g" | sed 's/,]/]/g' | sed 's/\[,/[/g') |
|||
_debug_sotoon "Remaining records after removal: $_remaining_records" |
|||
|
|||
# If no records remain, set to null to remove the subdomain entirely |
|||
if [ "$_remaining_records" = "[]" ] || [ -z "$_remaining_records" ]; then |
|||
_dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":null}}}" |
|||
else |
|||
_dns_record="{\"spec\":{\"records\":{\"$_sub_domain\":$_remaining_records}}}" |
|||
fi |
|||
|
|||
_debug_sotoon "Remove record payload: $_dns_record" |
|||
|
|||
# Use PATCH to remove the record from the domain zone |
|||
if _sotoon_rest PATCH "$_domain_id" "$_dns_record"; then |
|||
_info_sotoon "Record removed, OK" |
|||
return 0 |
|||
else |
|||
_debug_sotoon "Response: $response" |
|||
_err_sotoon "Error removing record" |
|||
return 1 |
|||
fi |
|||
} |
|||
|
|||
#################### Private functions below ################################## |
|||
|
|||
_get_root() { |
|||
domain=$1 |
|||
i=1 |
|||
p=1 |
|||
|
|||
_debug_sotoon "Getting root domain for: $domain" |
|||
_debug_sotoon "Sotoon WorkspaceUUID: $Sotoon_WorkspaceUUID" |
|||
|
|||
while true; do |
|||
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) |
|||
_debug_sotoon "Checking domain part: $h" |
|||
|
|||
if [ -z "$h" ]; then |
|||
#not valid |
|||
_err_sotoon "Could not find valid domain" |
|||
return 1 |
|||
fi |
|||
|
|||
_debug_sotoon "Fetching domain zones from Sotoon API" |
|||
if ! _sotoon_rest GET ""; then |
|||
_err_sotoon "Failed to get domain zones from Sotoon API" |
|||
_err_sotoon "Please check your Sotoon_Token, Sotoon_WorkspaceUUID" |
|||
return 1 |
|||
fi |
|||
|
|||
_debug2_sotoon "API Response: $response" |
|||
|
|||
# Check if the response contains our domain |
|||
# Sotoon API uses Kubernetes CRD format with spec.origin for domain matching |
|||
if _contains "$response" "\"origin\":\"$h\""; then |
|||
_debug_sotoon "Found domain by origin: $h" |
|||
|
|||
# In Kubernetes CRD format, the metadata.name is the resource identifier |
|||
# The name can be either: |
|||
# 1. Same as origin |
|||
# 2. Origin with dots replaced by hyphens |
|||
# We check both patterns in the response to determine which one exists |
|||
|
|||
# Convert origin to hyphenated version for checking |
|||
_h_hyphenated=$(echo "$h" | tr '.' '-') |
|||
|
|||
# Check if the hyphenated name exists in the response |
|||
if _contains "$response" "\"name\":\"$_h_hyphenated\""; then |
|||
_domain_id="$_h_hyphenated" |
|||
_debug_sotoon "Found domain ID (hyphenated): $_domain_id" |
|||
# Check if the origin itself is used as name |
|||
elif _contains "$response" "\"name\":\"$h\""; then |
|||
_domain_id="$h" |
|||
_debug_sotoon "Found domain ID (same as origin): $_domain_id" |
|||
else |
|||
# Fallback: use the hyphenated version (more common) |
|||
_domain_id="$_h_hyphenated" |
|||
_debug_sotoon "Using hyphenated domain ID as fallback: $_domain_id" |
|||
fi |
|||
|
|||
if [ -n "$_domain_id" ]; then |
|||
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p") |
|||
_domain=$h |
|||
_debug_sotoon "Domain ID (metadata.name): $_domain_id" |
|||
_debug_sotoon "Sub domain: $_sub_domain" |
|||
_debug_sotoon "Domain (origin): $_domain" |
|||
return 0 |
|||
fi |
|||
_err_sotoon "Found domain $h but could not extract domain ID" |
|||
return 1 |
|||
fi |
|||
p=$i |
|||
i=$(_math "$i" + 1) |
|||
done |
|||
return 1 |
|||
} |
|||
|
|||
_sotoon_rest() { |
|||
mtd="$1" |
|||
resource_id="$2" |
|||
data="$3" |
|||
|
|||
token_trimmed=$(echo "$Sotoon_Token" | tr -d '"') |
|||
|
|||
# Construct the API endpoint |
|||
_api_path="$SOTOON_API_URL/workspaces/$Sotoon_WorkspaceUUID/domainzones" |
|||
|
|||
if [ -n "$resource_id" ]; then |
|||
_api_path="$_api_path/$resource_id" |
|||
fi |
|||
|
|||
_debug_sotoon "API Path: $_api_path" |
|||
_debug_sotoon "Method: $mtd" |
|||
|
|||
# Set authorization header - Sotoon API uses Bearer token |
|||
export _H1="Authorization: Bearer $token_trimmed" |
|||
|
|||
if [ "$mtd" = "GET" ]; then |
|||
# GET request |
|||
_debug_sotoon "GET" "$_api_path" |
|||
response="$(_get "$_api_path")" |
|||
elif [ "$mtd" = "PATCH" ]; then |
|||
# PATCH Request |
|||
export _H2="Content-Type: application/merge-patch+json" |
|||
_debug_sotoon data "$data" |
|||
response="$(_post "$data" "$_api_path" "" "$mtd")" |
|||
else |
|||
_err_sotoon "Unknown method: $mtd" |
|||
return 1 |
|||
fi |
|||
|
|||
_debug2_sotoon response "$response" |
|||
return 0 |
|||
} |
|||
|
|||
#Wrappers for logging |
|||
_info_sotoon() { |
|||
_info "[Sotoon]" "$@" |
|||
} |
|||
|
|||
_err_sotoon() { |
|||
_err "[Sotoon]" "$@" |
|||
} |
|||
|
|||
_debug_sotoon() { |
|||
_debug "[Sotoon]" "$@" |
|||
} |
|||
|
|||
_debug2_sotoon() { |
|||
_debug2 "[Sotoon]" "$@" |
|||
} |
|||
@ -0,0 +1,212 @@ |
|||
#!/usr/bin/env sh |
|||
# shellcheck disable=SC2034 |
|||
dns_spaceship_info='Spaceship.com |
|||
Site: Spaceship.com |
|||
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_spaceship |
|||
Options: |
|||
SPACESHIP_API_KEY API Key |
|||
SPACESHIP_API_SECRET API Secret |
|||
SPACESHIP_ROOT_DOMAIN Root domain. Manually specify the root domain if auto-detection fails. Optional. |
|||
Issues: github.com/acmesh-official/acme.sh/issues/6304 |
|||
Author: Meow <@Meo597> |
|||
' |
|||
|
|||
# Spaceship API |
|||
# https://docs.spaceship.dev/ |
|||
|
|||
######## Public functions ##################### |
|||
|
|||
SPACESHIP_API_BASE="https://spaceship.dev/api/v1" |
|||
|
|||
# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" |
|||
# Used to add txt record |
|||
dns_spaceship_add() { |
|||
fulldomain="$1" |
|||
txtvalue="$2" |
|||
|
|||
_info "Adding TXT record for $fulldomain with value $txtvalue" |
|||
|
|||
# Initialize API credentials and headers |
|||
if ! _spaceship_init; then |
|||
return 1 |
|||
fi |
|||
|
|||
# Detect root zone |
|||
if ! _get_root "$fulldomain"; then |
|||
return 1 |
|||
fi |
|||
|
|||
# Extract subdomain part relative to root domain |
|||
subdomain=$(echo "$fulldomain" | sed "s/\.$_domain$//") |
|||
if [ "$subdomain" = "$fulldomain" ]; then |
|||
_err "Failed to extract subdomain from $fulldomain relative to root domain $_domain" |
|||
return 1 |
|||
fi |
|||
_debug "Extracted subdomain: $subdomain for root domain: $_domain" |
|||
|
|||
# Escape txtvalue to prevent JSON injection (e.g., quotes in txtvalue) |
|||
escaped_txtvalue=$(echo "$txtvalue" | sed 's/"/\\"/g') |
|||
|
|||
# Prepare payload and URL for adding TXT record |
|||
# Note: 'name' in payload uses subdomain (e.g., _acme-challenge.sub) as required by Spaceship API |
|||
payload="{\"force\": true, \"items\": [{\"type\": \"TXT\", \"name\": \"$subdomain\", \"value\": \"$escaped_txtvalue\", \"ttl\": 600}]}" |
|||
url="$SPACESHIP_API_BASE/dns/records/$_domain" |
|||
|
|||
# Send API request |
|||
if _spaceship_api_request "PUT" "$url" "$payload"; then |
|||
_info "Successfully added TXT record for $fulldomain" |
|||
return 0 |
|||
else |
|||
_err "Failed to add TXT record. If the domain $_domain is incorrect, set SPACESHIP_ROOT_DOMAIN to the correct root domain." |
|||
return 1 |
|||
fi |
|||
} |
|||
|
|||
# Usage: fulldomain txtvalue |
|||
# Used to remove the txt record after validation |
|||
dns_spaceship_rm() { |
|||
fulldomain="$1" |
|||
txtvalue="$2" |
|||
|
|||
_info "Removing TXT record for $fulldomain with value $txtvalue" |
|||
|
|||
# Initialize API credentials and headers |
|||
if ! _spaceship_init; then |
|||
return 1 |
|||
fi |
|||
|
|||
# Detect root zone |
|||
if ! _get_root "$fulldomain"; then |
|||
return 1 |
|||
fi |
|||
|
|||
# Extract subdomain part relative to root domain |
|||
subdomain=$(echo "$fulldomain" | sed "s/\.$_domain$//") |
|||
if [ "$subdomain" = "$fulldomain" ]; then |
|||
_err "Failed to extract subdomain from $fulldomain relative to root domain $_domain" |
|||
return 1 |
|||
fi |
|||
_debug "Extracted subdomain: $subdomain for root domain: $_domain" |
|||
|
|||
# Escape txtvalue to prevent JSON injection |
|||
escaped_txtvalue=$(echo "$txtvalue" | sed 's/"/\\"/g') |
|||
|
|||
# Prepare payload and URL for deleting TXT record |
|||
# Note: 'name' in payload uses subdomain (e.g., _acme-challenge.sub) as required by Spaceship API |
|||
payload="[{\"type\": \"TXT\", \"name\": \"$subdomain\", \"value\": \"$escaped_txtvalue\"}]" |
|||
url="$SPACESHIP_API_BASE/dns/records/$_domain" |
|||
|
|||
# Send API request |
|||
if _spaceship_api_request "DELETE" "$url" "$payload"; then |
|||
_info "Successfully deleted TXT record for $fulldomain" |
|||
return 0 |
|||
else |
|||
_err "Failed to delete TXT record. If the domain $_domain is incorrect, set SPACESHIP_ROOT_DOMAIN to the correct root domain." |
|||
return 1 |
|||
fi |
|||
} |
|||
|
|||
#################### Private functions below ################################## |
|||
|
|||
_spaceship_init() { |
|||
SPACESHIP_API_KEY="${SPACESHIP_API_KEY:-$(_readaccountconf_mutable SPACESHIP_API_KEY)}" |
|||
SPACESHIP_API_SECRET="${SPACESHIP_API_SECRET:-$(_readaccountconf_mutable SPACESHIP_API_SECRET)}" |
|||
|
|||
if [ -z "$SPACESHIP_API_KEY" ] || [ -z "$SPACESHIP_API_SECRET" ]; then |
|||
_err "Spaceship API credentials are not set. Please set SPACESHIP_API_KEY and SPACESHIP_API_SECRET." |
|||
_err "Ensure \"$LE_CONFIG_HOME\" directory has restricted permissions (chmod 700 \"$LE_CONFIG_HOME\") to protect credentials." |
|||
return 1 |
|||
fi |
|||
|
|||
# Save credentials to account config for future renewals |
|||
_saveaccountconf_mutable SPACESHIP_API_KEY "$SPACESHIP_API_KEY" |
|||
_saveaccountconf_mutable SPACESHIP_API_SECRET "$SPACESHIP_API_SECRET" |
|||
|
|||
# Set common headers for API requests |
|||
export _H1="X-API-Key: $SPACESHIP_API_KEY" |
|||
export _H2="X-API-Secret: $SPACESHIP_API_SECRET" |
|||
export _H3="Content-Type: application/json" |
|||
return 0 |
|||
} |
|||
|
|||
_get_root() { |
|||
domain="$1" |
|||
|
|||
# Check manual override |
|||
SPACESHIP_ROOT_DOMAIN="${SPACESHIP_ROOT_DOMAIN:-$(_readdomainconf SPACESHIP_ROOT_DOMAIN)}" |
|||
if [ -n "$SPACESHIP_ROOT_DOMAIN" ]; then |
|||
_domain="$SPACESHIP_ROOT_DOMAIN" |
|||
_debug "Using manually specified or saved root domain: $_domain" |
|||
_savedomainconf SPACESHIP_ROOT_DOMAIN "$SPACESHIP_ROOT_DOMAIN" |
|||
return 0 |
|||
fi |
|||
|
|||
_debug "Detecting root zone for '$domain'" |
|||
|
|||
i=1 |
|||
p=1 |
|||
while true; do |
|||
_cutdomain=$(printf "%s" "$domain" | cut -d . -f "$i"-100) |
|||
|
|||
_debug "Attempt i=$i: Checking if '$_cutdomain' is root zone (cut ret=$?)" |
|||
|
|||
if [ -z "$_cutdomain" ]; then |
|||
_debug "Cut resulted in empty string, root zone not found." |
|||
break |
|||
fi |
|||
|
|||
# Call the API to check if this _cutdomain is a manageable zone |
|||
if _spaceship_api_request "GET" "$SPACESHIP_API_BASE/dns/records/$_cutdomain?take=1&skip=0"; then |
|||
# API call succeeded (HTTP 200 OK for GET /dns/records) |
|||
_domain="$_cutdomain" |
|||
_debug "Root zone found: '$_domain'" |
|||
|
|||
# Save the detected root domain |
|||
_savedomainconf SPACESHIP_ROOT_DOMAIN "$_domain" |
|||
_info "Root domain '$_domain' saved to configuration for future use." |
|||
|
|||
return 0 |
|||
fi |
|||
|
|||
_debug "API check failed for '$_cutdomain'. Continuing search." |
|||
|
|||
p=$i |
|||
i=$((i + 1)) |
|||
done |
|||
|
|||
_err "Could not detect root zone for '$domain'. Please set SPACESHIP_ROOT_DOMAIN manually." |
|||
return 1 |
|||
} |
|||
|
|||
_spaceship_api_request() { |
|||
method="$1" |
|||
url="$2" |
|||
payload="$3" |
|||
|
|||
_debug2 "Sending $method request to $url with payload $payload" |
|||
if [ "$method" = "GET" ]; then |
|||
response="$(_get "$url")" |
|||
else |
|||
response="$(_post "$payload" "$url" "" "$method")" |
|||
fi |
|||
|
|||
if [ "$?" != "0" ]; then |
|||
_err "API request failed. Response: $response" |
|||
return 1 |
|||
fi |
|||
|
|||
_debug2 "API response body: $response" |
|||
|
|||
if [ "$method" = "GET" ]; then |
|||
if _contains "$(_head_n 1 <"$HTTP_HEADER")" '200'; then |
|||
return 0 |
|||
fi |
|||
else |
|||
if _contains "$(_head_n 1 <"$HTTP_HEADER")" '204'; then |
|||
return 0 |
|||
fi |
|||
fi |
|||
|
|||
_debug2 "API response header: $HTTP_HEADER" |
|||
return 1 |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Write
Preview
Loading…
Cancel
Save
Reference in new issue