diff --git a/README.md b/README.md index fd867015..7a79bed4 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,18 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa) # [中文说明](https://github.com/Neilpang/acme.sh/wiki/%E8%AF%B4%E6%98%8E) +# Who are using **acme.sh** +- [FreeBSD.org](https://blog.crashed.org/letsencrypt-in-freebsd-org/) +- [ruby-china.org](https://ruby-china.org/topics/31983) +- [Proxmox](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x_and_newer)) +- [pfsense](https://github.com/pfsense/FreeBSD-ports/pull/89) +- [webfaction](https://community.webfaction.com/questions/19988/using-letsencrypt) +- [Loadbalancer.org](https://www.loadbalancer.org/blog/loadbalancer-org-with-lets-encrypt-quick-and-dirty) +- [discourse.org](https://meta.discourse.org/t/setting-up-lets-encrypt/40709) +- [Centminmod](http://centminmod.com/letsencrypt-acmetool-https.html) +- [splynx](https://forum.splynx.com/t/free-ssl-cert-for-splynx-lets-encrypt/297) +- [archlinux](https://aur.archlinux.org/packages/acme.sh-git/) +- [more...](https://github.com/Neilpang/acme.sh/wiki/Blogs-and-tutorials) # Tested OS @@ -149,17 +161,17 @@ You **MUST** use this command to copy the certs to the target files, **DO NOT** **Apache** example: ```bash acme.sh --install-cert -d example.com \ ---certpath /path/to/certfile/in/apache/cert.pem \ ---keypath /path/to/keyfile/in/apache/key.pem \ ---fullchainpath /path/to/fullchain/certfile/apache/fullchain.pem \ +--cert-file /path/to/certfile/in/apache/cert.pem \ +--key-file /path/to/keyfile/in/apache/key.pem \ +--fullchain-file /path/to/fullchain/certfile/apache/fullchain.pem \ --reloadcmd "service apache2 force-reload" ``` **Nginx** example: ```bash acme.sh --install-cert -d example.com \ ---keypath /path/to/keyfile/in/nginx/key.pem \ ---fullchainpath /path/to/fullchain/nginx/cert.pem \ +--key-file /path/to/keyfile/in/nginx/key.pem \ +--fullchain-file /path/to/fullchain/nginx/cert.pem \ --reloadcmd "service nginx force-reload" ``` @@ -295,6 +307,10 @@ You don't have to do anything manually! 1. cyon.ch 1. Domain-Offensive/Resellerinterface/Domainrobot API 1. Gandi LiveDNS API +1. Knot DNS API +1. DigitalOcean API (native) +1. ClouDNS.net API +1. Infoblox NIOS API (https://www.infoblox.com/) **More APIs coming soon...** @@ -313,7 +329,7 @@ Just set the `length` parameter with a prefix `ec-`. For example: -### Single domain ECC cerfiticate +### Single domain ECC certificate ```bash acme.sh --issue -w /home/wwwroot/example.com -d example.com --keylength ec-256 diff --git a/acme.sh b/acme.sh index 0313e0e1..7cb36afb 100755 --- a/acme.sh +++ b/acme.sh @@ -146,7 +146,7 @@ _dlg_versions() { echo "apache:" if [ "$_APACHECTL" ] && _exists "$_APACHECTL"; then - _APACHECTL -V 2>&1 + $_APACHECTL -V 2>&1 else echo "apache doesn't exists." fi @@ -299,6 +299,16 @@ _secure_debug3() { fi } +_upper_case() { + # shellcheck disable=SC2018,SC2019 + tr 'a-z' 'A-Z' +} + +_lower_case() { + # shellcheck disable=SC2018,SC2019 + tr 'A-Z' 'a-z' +} + _startswith() { _str="$1" _sub="$2" @@ -330,7 +340,7 @@ _hasfield() { _sep="," fi - for f in $(echo "$_str" | tr ',' ' '); do + for f in $(echo "$_str" | tr "$_sep" ' '); do if [ "$f" = "$_field" ]; then _debug2 "'$_str' contains '$_field'" return 0 #contains ok @@ -869,7 +879,7 @@ _sign() { if ! _signedECText="$($_sign_openssl | $ACME_OPENSSL_BIN asn1parse -inform DER)"; then _err "Sign failed: $_sign_openssl" _err "Key file: $keyfile" - _err "Key content:$(wc -l <"$keyfile") lises" + _err "Key content:$(wc -l <"$keyfile") lines" return 1 fi _debug3 "_signedECText" "$_signedECText" @@ -1121,8 +1131,12 @@ _ss() { elif netstat -help 2>&1 | grep -- '-P protocol' >/dev/null; then #for solaris netstat -an -P tcp | grep "\.$_port " | grep "LISTEN" - else + elif netstat -help 2>&1 | grep "\-p" >/dev/null; then + #for full linux netstat -ntpl | grep ":$_port " + else + #for busybox (embedded linux; no pid support) + netstat -ntl 2>/dev/null | grep ":$_port " fi fi return 0 @@ -1279,12 +1293,12 @@ _url_replace() { } _time2str() { - #BSD + #Linux if date -u -d@"$1" 2>/dev/null; then return fi - #Linux + #BSD if date -u -r "$1" 2>/dev/null; then return fi @@ -1469,7 +1483,9 @@ _inithttp() { _ACME_CURL="$_ACME_CURL --trace-ascii $_CURL_DUMP " fi - if [ "$CA_BUNDLE" ]; then + if [ "$CA_PATH" ]; then + _ACME_CURL="$_ACME_CURL --capath $CA_PATH " + elif [ "$CA_BUNDLE" ]; then _ACME_CURL="$_ACME_CURL --cacert $CA_BUNDLE " fi @@ -1480,8 +1496,10 @@ _inithttp() { if [ "$DEBUG" ] && [ "$DEBUG" -ge "2" ]; then _ACME_WGET="$_ACME_WGET -d " fi - if [ "$CA_BUNDLE" ]; then - _ACME_WGET="$_ACME_WGET --ca-certificate $CA_BUNDLE " + if [ "$CA_PATH" ]; then + _ACME_WGET="$_ACME_WGET --ca-directory=$CA_PATH " + elif [ "$CA_BUNDLE" ]; then + _ACME_WGET="$_ACME_WGET --ca-certificate=$CA_BUNDLE " fi fi @@ -2240,16 +2258,16 @@ _initpath() { fi if [ -z "$TLS_CONF" ]; then - TLS_CONF="$DOMAIN_PATH/tls.valdation.conf" + TLS_CONF="$DOMAIN_PATH/tls.validation.conf" fi if [ -z "$TLS_CERT" ]; then - TLS_CERT="$DOMAIN_PATH/tls.valdation.cert" + TLS_CERT="$DOMAIN_PATH/tls.validation.cert" fi if [ -z "$TLS_KEY" ]; then - TLS_KEY="$DOMAIN_PATH/tls.valdation.key" + TLS_KEY="$DOMAIN_PATH/tls.validation.key" fi if [ -z "$TLS_CSR" ]; then - TLS_CSR="$DOMAIN_PATH/tls.valdation.csr" + TLS_CSR="$DOMAIN_PATH/tls.validation.csr" fi } @@ -2367,7 +2385,7 @@ _setApache() { _debug "Backup apache config file" "$httpdconf" if ! cp "$httpdconf" "$APACHE_CONF_BACKUP_DIR/"; then _err "Can not backup apache config file, so abort. Don't worry, the apache config is not changed." - _err "This might be a bug of $PROJECT_NAME , pleae report issue: $PROJECT" + _err "This might be a bug of $PROJECT_NAME , please report issue: $PROJECT" return 1 fi _info "JFYI, Config file $httpdconf is backuped to $APACHE_CONF_BACKUP_DIR/$httpdconfname" @@ -2462,7 +2480,7 @@ _setNginx() { fi _debug "Start detect nginx conf for $_d from:$_start_f" if ! _checkConf "$_d" "$_start_f"; then - "Can not find conf file for domain $d" + _err "Can not find conf file for domain $d" return 1 fi _info "Found conf file: $FOUND_REAL_NGINX_CONF" @@ -2546,7 +2564,7 @@ _checkConf() { if [ ! -f "$2" ] && ! echo "$2" | grep '*$' >/dev/null && echo "$2" | grep '*' >/dev/null; then _debug "wildcard" for _w_f in $2; do - if _checkConf "$1" "$_w_f"; then + if [ -f "$_w_f"] && _checkConf "$1" "$_w_f"; then return 0 fi done @@ -2559,9 +2577,9 @@ _checkConf() { FOUND_REAL_NGINX_CONF="$2" return 0 fi - if grep "^ *include *.*;" "$2" >/dev/null; then + if cat "$2" | tr "\t" " " | grep "^ *include *.*;" >/dev/null; then _debug "Try include files" - for included in $(grep "^ *include *.*;" "$2" | sed "s/include //" | tr -d " ;"); do + for included in $(cat "$2" | tr "\t" " " | grep "^ *include *.*;" | sed "s/include //" | tr -d " ;"); do _debug "check included $included" if _checkConf "$1" "$included"; then return 0 @@ -2865,7 +2883,7 @@ _on_issue_err() { uri=$(echo "$ventry" | cut -d "$sep" -f 3) vtype=$(echo "$ventry" | cut -d "$sep" -f 4) _currentRoot=$(echo "$ventry" | cut -d "$sep" -f 5) - __trigger_validaton "$uri" "$keyauthorization" + __trigger_validation "$uri" "$keyauthorization" done ) fi @@ -3087,7 +3105,7 @@ __get_domain_new_authz() { } #uri keyAuthorization -__trigger_validaton() { +__trigger_validation() { _debug2 "tigger domain validation." _t_url="$1" _debug2 _t_url "$_t_url" @@ -3467,9 +3485,12 @@ issue() { if [ ! "$usingApache" ]; then if webroot_owner=$(_stat "$_currentRoot"); then _debug "Changing owner/group of .well-known to $webroot_owner" - chown -R "$webroot_owner" "$_currentRoot/.well-known" + if ! _exec "chown -R \"$webroot_owner\" \"$_currentRoot/.well-known\""; then + _debug "$(cat "$_EXEC_TEMP_ERR")" + _exec_err >/dev/null 2>&1 + fi else - _debug "not chaning owner/group of webroot" + _debug "not changing owner/group of webroot" fi fi @@ -3510,7 +3531,7 @@ issue() { fi fi - if ! __trigger_validaton "$uri" "$keyauthorization"; then + if ! __trigger_validation "$uri" "$keyauthorization"; then _err "$d:Can not get challenge: $response" _clearupwebbroot "$_currentRoot" "$removelevel" "$token" _clearup @@ -3690,6 +3711,12 @@ issue() { _clearaccountconf "CA_BUNDLE" fi + if [ "$CA_PATH" ]; then + _saveaccountconf CA_PATH "$CA_PATH" + else + _clearaccountconf "CA_PATH" + fi + if [ "$HTTPS_INSECURE" ]; then _saveaccountconf HTTPS_INSECURE "$HTTPS_INSECURE" else @@ -4008,7 +4035,7 @@ deploy() { installcert() { _main_domain="$1" if [ -z "$_main_domain" ]; then - _usage "Usage: $PROJECT_ENTRY --installcert -d domain.com [--ecc] [--certpath cert-file-path] [--keypath key-file-path] [--capath ca-cert-file-path] [ --reloadCmd reloadCmd] [--fullchainpath fullchain-path]" + _usage "Usage: $PROJECT_ENTRY --installcert -d domain.com [--ecc] [--cert-file cert-file-path] [--key-file key-file-path] [--ca-file ca-cert-file-path] [ --reloadCmd reloadCmd] [--fullchain-file fullchain-path]" return 1 fi @@ -4107,6 +4134,7 @@ _installcert() { export CERT_KEY_PATH export CA_CERT_PATH export CERT_FULLCHAIN_PATH + export Le_Domain cd "$DOMAIN_PATH" && eval "$_reload_cmd" ); then _info "$(__green "Reload success")" @@ -4618,7 +4646,7 @@ install() { #Modify shebang if _exists bash; then _info "Good, bash is found, so change the shebang to use bash as preferred." - _shebang='#!/usr/bin/env bash' + _shebang='#!'"$(env bash -c "command -v bash")" _setShebang "$LE_WORKING_DIR/$PROJECT_ENTRY" "$_shebang" for subf in $_SUB_FOLDERS; do if [ -d "$LE_WORKING_DIR/$subf" ]; then @@ -4758,10 +4786,10 @@ Parameters: These parameters are to install the cert to nginx/apache or anyother server after issue/renew a cert: - --certpath /path/to/real/cert/file After issue/renew, the cert will be copied to this path. - --keypath /path/to/real/key/file After issue/renew, the key will be copied to this path. - --capath /path/to/real/ca/file After issue/renew, the intermediate cert will be copied to this path. - --fullchainpath /path/to/fullchain/file After issue/renew, the fullchain cert will be copied to this path. + --cert-file After issue/renew, the cert will be copied to this path. + --key-file After issue/renew, the key will be copied to this path. + --ca-file After issue/renew, the intermediate cert will be copied to this path. + --fullchain-file After issue/renew, the fullchain cert will be copied to this path. --reloadcmd \"service nginx reload\" After issue/renew, it's used to reload the server. @@ -4779,12 +4807,13 @@ Parameters: --listraw Only used for '--list' command, list the certs in raw format. --stopRenewOnError, -se Only valid for '--renew-all' command. Stop if one cert has error in renewal. --insecure Do not check the server certificate, in some devices, the api server's certificate may not be trusted. - --ca-bundle Specifices the path to the CA certificate bundle to verify api server's certificate. + --ca-bundle Specifies the path to the CA certificate bundle to verify api server's certificate. + --ca-path Specifies directory containing CA certificates in PEM format, used by wget or curl. --nocron Only valid for '--install' command, which means: do not install the default cron job. In this case, the certs will not be renewed automatically. --ecc Specifies to use the ECC cert. Valid for '--install-cert', '--renew', '--revoke', '--toPkcs' and '--createCSR' --csr Specifies the input csr. --pre-hook Command to be run before obtaining any certificates. - --post-hook Command to be run after attempting to obtain/renew certificates. No matter the obain/renew is success or failed. + --post-hook Command to be run after attempting to obtain/renew certificates. No matter the obtain/renew is success or failed. --renew-hook Command to be run once for each successfully renewed certificate. --deploy-hook The hook file to deploy cert --ocsp-must-staple, --ocsp Generate ocsp must Staple extension. @@ -4886,10 +4915,10 @@ _process() { _webroot="" _keylength="" _accountkeylength="" - _certpath="" - _keypath="" - _capath="" - _fullchainpath="" + _cert_file="" + _key_file="" + _ca_file="" + _fullchain_file="" _reloadcmd="" _password="" _accountconf="" @@ -4905,6 +4934,7 @@ _process() { _stopRenewOnError="" #_insecure="" _ca_bundle="" + _ca_path="" _nocron="" _ecc="" _csr="" @@ -5130,20 +5160,20 @@ _process() { shift ;; - --certpath) - _certpath="$2" + --cert-file | --certpath) + _cert_file="$2" shift ;; - --keypath) - _keypath="$2" + --key-file | --keypath) + _key_file="$2" shift ;; - --capath) - _capath="$2" + --ca-file | --capath) + _ca_file="$2" shift ;; - --fullchainpath) - _fullchainpath="$2" + --fullchain-file | --fullchainpath) + _fullchain_file="$2" shift ;; --reloadcmd | --reloadCmd) @@ -5219,6 +5249,11 @@ _process() { CA_BUNDLE="$_ca_bundle" shift ;; + --ca-path) + _ca_path="$2" + CA_PATH="$_ca_path" + shift + ;; --nocron) _nocron="1" ;; @@ -5360,7 +5395,7 @@ _process() { uninstall) uninstall "$_nocron" ;; upgrade) upgrade ;; issue) - issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_certpath" "$_keypath" "$_capath" "$_reloadcmd" "$_fullchainpath" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" + issue "$_webroot" "$_domain" "$_altdomains" "$_keylength" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" ;; deploy) deploy "$_domain" "$_deploy_hook" "$_ecc" @@ -5372,7 +5407,7 @@ _process() { showcsr "$_csr" "$_domain" ;; installcert) - installcert "$_domain" "$_certpath" "$_keypath" "$_capath" "$_reloadcmd" "$_fullchainpath" "$_ecc" + installcert "$_domain" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_ecc" ;; renew) renew "$_domain" "$_ecc" diff --git a/deploy/README.md b/deploy/README.md index d0235667..e4a62b55 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -72,8 +72,14 @@ export DEPLOY_EXIM4_RELOAD="/etc/init.d/exim4 restart" acme.sh --deploy -d ftp.example.com --deploy-hook exim4 ``` -## 6. Deploy the cert to local strongswan server. +## 6. Deploy the cert to OSX Keychain ```sh -acme.sh --deploy -d ftp.example.com --deploy-hook strongswan +acme.sh --deploy -d ftp.example.com --deploy-hook keychain ``` + +## 7. Deploy the cert to local strongswan server. + +```sh +acme.sh --deploy -d ftp.example.com --deploy-hook strongswan +``` \ No newline at end of file diff --git a/deploy/exim4.sh b/deploy/exim4.sh index bf92b438..573f762b 100644 --- a/deploy/exim4.sh +++ b/deploy/exim4.sh @@ -79,7 +79,7 @@ exim4_deploy() { _info "Restore conf success" eval "$_reload" else - _err "Opps, error restore exim4 conf, please report bug to us." + _err "Oops, error restore exim4 conf, please report bug to us." fi return 1 fi @@ -105,7 +105,7 @@ exim4_deploy() { _info "Restore conf success" eval "$_reload" else - _err "Opps, error restore exim4 conf, please report bug to us." + _err "Oops, error restore exim4 conf, please report bug to us." fi return 1 fi diff --git a/deploy/keychain.sh b/deploy/keychain.sh new file mode 100644 index 00000000..a99ed465 --- /dev/null +++ b/deploy/keychain.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env sh + +#Here is a sample custom api script. +#This file name is "myapi.sh" +#So, here must be a method myapi_deploy() +#Which will be called by acme.sh to deploy the cert +#returns 0 means success, otherwise error. + +######## Public functions ##################### + +#domain keyfile certfile cafile fullchain +keychain_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" + + /usr/bin/security import "$_ckey" -k "/Library/Keychains/System.keychain" + /usr/bin/security import "$_ccert" -k "/Library/Keychains/System.keychain" + /usr/bin/security import "$_cca" -k "/Library/Keychains/System.keychain" + /usr/bin/security import "$_cfullchain" -k "/Library/Keychains/System.keychain" + + return 0 +} diff --git a/deploy/kong.sh b/deploy/kong.sh index 3b9c5c79..e1873f35 100755 --- a/deploy/kong.sh +++ b/deploy/kong.sh @@ -43,7 +43,7 @@ kong_deploy() { #Save kong url if it's succesful (First run case) _saveaccountconf KONG_URL "$KONG_URL" #Generate DEIM - delim="-----MultipartDelimeter$(date "+%s%N")" + delim="-----MultipartDelimiter$(date "+%s%N")" nl="\015\012" #Set Header _H1="Content-Type: multipart/form-data; boundary=$delim" @@ -72,7 +72,7 @@ kong_deploy() { response=$(_post "$content" "$KONG_URL/apis/$uuid/plugins/$ssl_uuid" "" "PATCH") fi if ! [ "$(echo "$response" | _egrep_o "ssl")" = "ssl" ]; then - _err "An error occured with cert upload. Check response:" + _err "An error occurred with cert upload. Check response:" _err "$response" return 1 fi diff --git a/deploy/vsftpd.sh b/deploy/vsftpd.sh index 1c6410a6..ed44e709 100644 --- a/deploy/vsftpd.sh +++ b/deploy/vsftpd.sh @@ -76,7 +76,7 @@ vsftpd_deploy() { _info "Restore conf success" eval "$_reload" else - _err "Opps, error restore vsftpd conf, please report bug to us." + _err "Oops, error restore vsftpd conf, please report bug to us." fi return 1 fi @@ -102,7 +102,7 @@ vsftpd_deploy() { _info "Restore conf success" eval "$_reload" else - _err "Opps, error restore vsftpd conf, please report bug to us." + _err "Oops, error restore vsftpd conf, please report bug to us." fi return 1 fi diff --git a/dnsapi/README.md b/dnsapi/README.md index 18c1ca9f..9eb77915 100644 --- a/dnsapi/README.md +++ b/dnsapi/README.md @@ -302,7 +302,7 @@ acme.sh --issue --dns dns_freedns -d example.com -d www.example.com ``` Note that you cannot use acme.sh automatic DNS validation for FreeDNS public domains or for a subdomain that -you create under a FreeDNS public domain. You must own the top level domain in order to automaitcally +you create under a FreeDNS public domain. You must own the top level domain in order to automatically validate with acme.sh at FreeDNS. ## 16. Use cyon.ch @@ -349,6 +349,95 @@ Ok, let's issue a cert now: acme.sh --issue --dns dns_gandi_livedns -d example.com -d www.example.com ``` +## 19. Use Knot (knsupdate) DNS API to automatically issue cert + +First, generate a TSIG key for updating the zone. + +``` +keymgr tsig generate acme_key algorithm hmac-sha512 > /etc/knot/acme.key +``` + +Include this key in your knot configuration file. + +``` +include: /etc/knot/acme.key +``` + +Next, configure your zone to allow dynamic updates. + +Dynamic updates for the zone are allowed via proper ACL rule with the `update` action. For in-depth instructions, please see [Knot DNS's documentation](https://www.knot-dns.cz/documentation/). + +``` +acl: + - id: acme_acl + address: 192.168.1.0/24 + key: acme_key + action: update + +zone: + - domain: example.com + file: example.com.zone + acl: acme_acl +``` + +Finally, make the DNS server and TSIG Key available to `acme.sh` + +``` +export KNOT_SERVER="dns.example.com" +export KNOT_KEY=`grep \# /etc/knot/acme.key | cut -d' ' -f2` +``` + +Ok, let's issue a cert now: +``` +acme.sh --issue --dns dns_knot -d example.com -d www.example.com +``` + +The `KNOT_SERVER` and `KNOT_KEY` settings will be saved in `~/.acme.sh/account.conf` and will be reused when needed. + +## 20. Use DigitalOcean API (native) + +You need to obtain a read and write capable API key from your DigitalOcean account. See: https://www.digitalocean.com/help/api/ + +``` +export DO_API_KEY="75310dc4ca779ac39a19f6355db573b49ce92ae126553ebd61ac3a3ae34834cc" +``` + +Ok, let's issue a cert now: +``` +acme.sh --issue --dns dns_dgon -d example.com -d www.example.com +``` + +## 21. Use ClouDNS.net API + +You need to set the HTTP API user ID and password credentials. See: https://www.cloudns.net/wiki/article/42/ + +``` +export CLOUDNS_AUTH_ID=XXXXX +export CLOUDNS_AUTH_PASSWORD="YYYYYYYYY" +``` + +Ok, let's issue a cert now: +``` +acme.sh --issue --dns dns_cloudns -d example.com -d www.example.com +``` + +## 22. Use Infoblox API + +First you need to create/obtain API credentials on your Infoblox appliance. + +``` +export Infoblox_Creds="username:password" +export Infoblox_Server="ip or fqdn of infoblox appliance" +``` + +Ok, let's issue a cert now: +``` +acme.sh --issue --dns dns_infoblox -d example.com -d www.example.com +``` + +Note: This script will automatically create and delete the ephemeral txt record. +The `Infoblox_Creds` and `Infoblox_Server` will be saved in `~/.acme.sh/account.conf` and will be reused when needed. + # Use custom API If your API is not supported yet, you can write your own DNS API. diff --git a/dnsapi/dns_aws.sh b/dnsapi/dns_aws.sh index 84aa28d3..d5d52de0 100755 --- a/dnsapi/dns_aws.sh +++ b/dnsapi/dns_aws.sh @@ -143,7 +143,7 @@ aws_rest() { CanonicalHeaders="host:$aws_host\nx-amz-date:$RequestDate\n" SignedHeaders="host;x-amz-date" if [ -n "$AWS_SESSION_TOKEN" ]; then - export _H2="x-amz-security-token: $AWS_SESSION_TOKEN" + export _H3="x-amz-security-token: $AWS_SESSION_TOKEN" CanonicalHeaders="${CanonicalHeaders}x-amz-security-token:$AWS_SESSION_TOKEN\n" SignedHeaders="${SignedHeaders};x-amz-security-token" fi @@ -204,8 +204,8 @@ aws_rest() { Authorization="$Algorithm Credential=$AWS_ACCESS_KEY_ID/$CredentialScope, SignedHeaders=$SignedHeaders, Signature=$signature" _debug2 Authorization "$Authorization" - _H3="Authorization: $Authorization" - _debug _H3 "$_H3" + _H2="Authorization: $Authorization" + _debug _H2 "$_H2" url="$AWS_URL/$ep" diff --git a/dnsapi/dns_cloudns.sh b/dnsapi/dns_cloudns.sh new file mode 100755 index 00000000..f48a8052 --- /dev/null +++ b/dnsapi/dns_cloudns.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env sh + +# Author: Boyan Peychev +# Repository: https://github.com/ClouDNS/acme.sh/ + +#CLOUDNS_AUTH_ID=XXXXX +#CLOUDNS_AUTH_PASSWORD="YYYYYYYYY" +CLOUDNS_API="https://api.cloudns.net" + +######## Public functions ##################### + +#Usage: dns_cloudns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_cloudns_add() { + _info "Using cloudns" + + if ! _dns_cloudns_init_check; then + return 1 + fi + + zone="$(_dns_cloudns_get_zone_name "$1")" + if [ -z "$zone" ]; then + _err "Missing DNS zone at ClouDNS. Please log into your control panel and create the required DNS zone for the initial setup." + return 1 + fi + + host="$(echo "$1" | sed "s/\.$zone\$//")" + record=$2 + record_id=$(_dns_cloudns_get_record_id "$zone" "$host") + + _debug zone "$zone" + _debug host "$host" + _debug record "$record" + _debug record_id "$record_id" + + if [ -z "$record_id" ]; then + _info "Adding the TXT record for $1" + _dns_cloudns_http_api_call "dns/add-record.json" "domain-name=$zone&record-type=TXT&host=$host&record=$record&ttl=60" + if ! _contains "$response" "\"status\":\"Success\""; then + _err "Record cannot be added." + return 1 + fi + _info "Added." + else + _info "Updating the TXT record for $1" + _dns_cloudns_http_api_call "dns/mod-record.json" "domain-name=$zone&record-id=$record_id&record-type=TXT&host=$host&record=$record&ttl=60" + if ! _contains "$response" "\"status\":\"Success\""; then + _err "The TXT record for $1 cannot be updated." + return 1 + fi + _info "Updated." + fi + + return 0 +} + +#Usage: dns_cloudns_rm _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_cloudns_rm() { + _info "Using cloudns" + + if ! _dns_cloudns_init_check; then + return 1 + fi + + if [ -z "$zone" ]; then + zone="$(_dns_cloudns_get_zone_name "$1")" + if [ -z "$zone" ]; then + _err "Missing DNS zone at ClouDNS. Please log into your control panel and create the required DNS zone for the initial setup." + return 1 + fi + fi + + host="$(echo "$1" | sed "s/\.$zone\$//")" + record=$2 + record_id=$(_dns_cloudns_get_record_id "$zone" "$host") + + _debug zone "$zone" + _debug host "$host" + _debug record "$record" + _debug record_id "$record_id" + + if [ ! -z "$record_id" ]; then + _info "Deleting the TXT record for $1" + _dns_cloudns_http_api_call "dns/delete-record.json" "domain-name=$zone&record-id=$record_id" + if ! _contains "$response" "\"status\":\"Success\""; then + _err "The TXT record for $1 cannot be deleted." + return 1 + fi + _info "Deleted." + fi + return 0 +} + +#################### Private functions below ################################## +_dns_cloudns_init_check() { + if [ ! -z "$CLOUDNS_INIT_CHECK_COMPLETED" ]; then + return 0 + fi + + if [ -z "$CLOUDNS_AUTH_ID" ]; then + _err "CLOUDNS_AUTH_ID is not configured" + return 1 + fi + + if [ -z "$CLOUDNS_AUTH_PASSWORD" ]; then + _err "CLOUDNS_AUTH_PASSWORD is not configured" + return 1 + fi + + _dns_cloudns_http_api_call "dns/login.json" "" + + if ! _contains "$response" "\"status\":\"Success\""; then + _err "Invalid CLOUDNS_AUTH_ID or CLOUDNS_AUTH_PASSWORD. Please check your login credentials." + return 1 + fi + + CLOUDNS_INIT_CHECK_COMPLETED=1 + + return 0 +} + +_dns_cloudns_get_zone_name() { + i=2 + while true; do + zoneForCheck=$(printf "%s" "$1" | cut -d . -f $i-100) + + if [ -z "$zoneForCheck" ]; then + return 1 + fi + + _debug zoneForCheck "$zoneForCheck" + + _dns_cloudns_http_api_call "dns/get-zone-info.json" "domain-name=$zoneForCheck" + + if ! _contains "$response" "\"status\":\"Failed\""; then + echo "$zoneForCheck" + return 0 + fi + + i=$(_math "$i" + 1) + done + return 1 +} + +_dns_cloudns_get_record_id() { + _dns_cloudns_http_api_call "dns/records.json" "domain-name=$1&host=$2&type=TXT" + if _contains "$response" "\"id\":"; then + echo "$response" | cut -d '"' -f 2 + return 0 + fi + return 1 +} + +_dns_cloudns_http_api_call() { + method=$1 + + _debug CLOUDNS_AUTH_ID "$CLOUDNS_AUTH_ID" + _debug CLOUDNS_AUTH_PASSWORD "$CLOUDNS_AUTH_PASSWORD" + + if [ -z "$2" ]; then + data="auth-id=$CLOUDNS_AUTH_ID&auth-password=$CLOUDNS_AUTH_PASSWORD" + else + data="auth-id=$CLOUDNS_AUTH_ID&auth-password=$CLOUDNS_AUTH_PASSWORD&$2" + fi + + response="$(_get "$CLOUDNS_API/$method?$data")" + + _debug2 response "$response" + + return 0 +} diff --git a/dnsapi/dns_cx.sh b/dnsapi/dns_cx.sh index 2b6d5691..e2f0f099 100755 --- a/dnsapi/dns_cx.sh +++ b/dnsapi/dns_cx.sh @@ -209,8 +209,7 @@ _rest() { return 1 fi _debug2 response "$response" - if ! _contains "$response" '"message":"success"'; then - return 1 - fi - return 0 + + _contains "$response" '"code":1' + } diff --git a/dnsapi/dns_cyon.sh b/dnsapi/dns_cyon.sh index c096d8b0..d7ad712c 100644 --- a/dnsapi/dns_cyon.sh +++ b/dnsapi/dns_cyon.sh @@ -50,7 +50,7 @@ _cyon_load_credentials() { fi if [ -z "${CY_Username}" ] || [ -z "${CY_Password}" ]; then - # Dummy entries to satify script checker. + # Dummy entries to satisfy script checker. CY_Username="" CY_Password="" CY_OTP_Secret="" diff --git a/dnsapi/dns_dgon.sh b/dnsapi/dns_dgon.sh new file mode 100755 index 00000000..7e1f1fec --- /dev/null +++ b/dnsapi/dns_dgon.sh @@ -0,0 +1,205 @@ +#!/usr/bin/env sh + +## Will be called by acme.sh to add the txt record to your api system. +## returns 0 means success, otherwise error. + +## Author: thewer +## GitHub: https://github.com/gitwer/acme.sh + +## +## Environment Variables Required: +## +## DO_API_KEY="75310dc4ca779ac39a19f6355db573b49ce92ae126553ebd61ac3a3ae34834cc" +## + +##################### Public functions ##################### + +## Create the text record for validation. +## Usage: fulldomain txtvalue +## EG: "_acme-challenge.www.other.domain.com" "XKrxpRBosdq0HG9i01zxXp5CPBs" +dns_dgon_add() { + fulldomain="$(echo "$1" | _lower_case)" + txtvalue=$2 + _info "Using digitalocean dns validation - add record" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + ## save the env vars (key and domain split location) for later automated use + _saveaccountconf DO_API_KEY "$DO_API_KEY" + + ## split the domain for DO API + if ! _get_base_domain "$fulldomain"; then + _err "domain not found in your account for addition" + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + ## Set the header with our post type and key auth key + export _H1="Content-Type: application/json" + export _H2="Authorization: Bearer $DO_API_KEY" + PURL='https://api.digitalocean.com/v2/domains/'$_domain'/records' + PBODY='{"type":"TXT","name":"'$_sub_domain'","data":"'$txtvalue'"}' + + _debug PURL "$PURL" + _debug PBODY "$PBODY" + + ## the create request - post + ## args: BODY, URL, [need64, httpmethod] + response="$(_post "$PBODY" "$PURL")" + + ## check response + if [ "$?" != "0" ]; then + _err "error in response: $response" + return 1 + fi + _debug2 response "$response" + + ## finished correctly + return 0 +} + +## Remove the txt record after validation. +## Usage: fulldomain txtvalue +## EG: "_acme-challenge.www.other.domain.com" "XKrxpRBosdq0HG9i01zxXp5CPBs" +dns_dgon_rm() { + fulldomain="$(echo "$1" | _lower_case)" + txtvalue=$2 + _info "Using digitalocean dns validation - remove record" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + ## split the domain for DO API + if ! _get_base_domain "$fulldomain"; then + _err "domain not found in your account for removal" + return 1 + fi + _debug _sub_domain "$_sub_domain" + _debug _domain "$_domain" + + ## Set the header with our post type and key auth key + export _H1="Content-Type: application/json" + export _H2="Authorization: Bearer $DO_API_KEY" + ## get URL for the list of domains + ## may get: "links":{"pages":{"last":".../v2/domains/DOM/records?page=2","next":".../v2/domains/DOM/records?page=2"}} + GURL="https://api.digitalocean.com/v2/domains/$_domain/records" + + ## while we dont have a record ID we keep going + while [ -z "$record" ]; do + ## 1) get the URL + ## the create request - get + ## args: URL, [onlyheader, timeout] + domain_list="$(_get "$GURL")" + ## 2) find record + ## check for what we are looing for: "type":"A","name":"$_sub_domain" + record="$(echo "$domain_list" | _egrep_o "\"id\"\s*\:\s*\"*\d+\"*[^}]*\"name\"\s*\:\s*\"$_sub_domain\"[^}]*\"data\"\s*\:\s*\"$txtvalue\"")" + ## 3) check record and get next page + if [ -z "$record" ]; then + ## find the next page if we dont have a match + nextpage="$(echo "$domain_list" | _egrep_o "\"links\".*" | _egrep_o "\"next\".*" | _egrep_o "http.*page\=\d+")" + if [ -z "$nextpage" ]; then + _err "no record and no nextpage in digital ocean DNS removal" + return 1 + fi + _debug2 nextpage "$nextpage" + GURL="$nextpage" + fi + ## we break out of the loop when we have a record + done + + ## we found the record + rec_id="$(echo "$record" | _egrep_o "id\"\s*\:\s*\"*\d+" | _egrep_o "\d+")" + _debug rec_id "$rec_id" + + ## delete the record + ## delete URL for removing the one we dont want + DURL="https://api.digitalocean.com/v2/domains/$_domain/records/$rec_id" + + ## the create request - delete + ## args: BODY, URL, [need64, httpmethod] + response="$(_post "" "$DURL" "" "DELETE")" + + ## check response (sort of) + if [ "$?" != "0" ]; then + _err "error in remove response: $response" + return 1 + fi + _debug2 response "$response" + + ## finished correctly + return 0 +} + +##################### Private functions below ##################### + +## Split the domain provided into the "bade domain" and the "start prefix". +## This function searches for the longest subdomain in your account +## for the full domain given and splits it into the base domain (zone) +## and the prefix/record to be added/removed +## USAGE: fulldomain +## EG: "_acme-challenge.two.three.four.domain.com" +## returns +## _sub_domain="_acme-challenge.two" +## _domain="three.four.domain.com" *IF* zone "three.four.domain.com" exists +## if only "domain.com" exists it will return +## _sub_domain="_acme-challenge.two.three.four" +## _domain="domain.com" +_get_base_domain() { + # args + fulldomain="$(echo "$1" | tr '[:upper:]' '[:lower:]')" + _debug fulldomain "$fulldomain" + + # domain max legal length = 253 + MAX_DOM=255 + + ## get a list of domains for the account to check thru + ## Set the headers + export _H1="Content-Type: application/json" + export _H2="Authorization: Bearer $DO_API_KEY" + _debug DO_API_KEY "$DO_API_KEY" + ## get URL for the list of domains + ## havent seen this request paginated, tested with 18 domains (more requires manual requests with DO) + DOMURL="https://api.digitalocean.com/v2/domains" + + ## get the domain list (DO gives basically a full XFER!) + domain_list="$(_get "$DOMURL")" + + ## check response + if [ "$?" != "0" ]; then + _err "error in domain_list response: $domain_list" + return 1 + fi + _debug2 domain_list "$domain_list" + + ## for each shortening of our $fulldomain, check if it exists in the $domain_list + ## can never start on 1 (aka whole $fulldomain) as $fulldomain starts with "_acme-challenge" + i=2 + while [ $i -gt 0 ]; do + ## get next longest domain + _domain=$(printf "%s" "$fulldomain" | cut -d . -f "$i"-"$MAX_DOM") + ## check we got something back from our cut (or are we at the end) + if [ -z "$_domain" ]; then + ## we got to the end of the domain - invalid domain + _err "domain not found in DigitalOcean account" + return 1 + fi + ## we got part of a domain back - grep it out + found="$(echo "$domain_list" | _egrep_o "\"name\"\s*\:\s*\"$_domain\"")" + ## check if it exists + if [ ! -z "$found" ]; then + ## exists - exit loop returning the parts + sub_point=$(_math $i - 1) + _sub_domain=$(printf "%s" "$fulldomain" | cut -d . -f 1-"$sub_point") + _debug _domain "$_domain" + _debug _sub_domain "$_sub_domain" + return 0 + fi + ## increment cut point $i + i=$(_math $i + 1) + done + + ## we went through the entire domain zone list and dint find one that matched + ## doesnt look like we can add in the record + _err "domain not found in DigitalOcean account, but we should never get here" + return 1 +} diff --git a/dnsapi/dns_freedns.sh b/dnsapi/dns_freedns.sh index f30c8958..272a1243 100755 --- a/dnsapi/dns_freedns.sh +++ b/dnsapi/dns_freedns.sh @@ -10,7 +10,7 @@ # ######## Public functions ##################### -# Export FreeDNS userid and password in folowing variables... +# Export FreeDNS userid and password in following variables... # FREEDNS_User=username # FREEDNS_Password=password # login cookie is saved in acme account config file so userid / pw @@ -53,7 +53,7 @@ dns_freedns_add() { i="$(_math "$i" - 1)" sub_domain="$(echo "$fulldomain" | cut -d. -f -"$i")" - # Sometimes FreeDNS does not reurn the subdomain page but rather + # Sometimes FreeDNS does not return the subdomain page but rather # returns a page regarding becoming a premium member. This usually # happens after a period of inactivity. Immediately trying again # returns the correct subdomain page. So, we will try twice to @@ -65,7 +65,7 @@ dns_freedns_add() { htmlpage="$(_freedns_retrieve_subdomain_page "$FREEDNS_COOKIE")" if [ "$?" != "0" ]; then if [ "$using_cached_cookies" = "true" ]; then - _err "Has your FreeDNS username and password channged? If so..." + _err "Has your FreeDNS username and password changed? If so..." _err "Please export as FREEDNS_User / FREEDNS_Password and try again." fi return 1 @@ -112,7 +112,7 @@ dns_freedns_add() { # not produce accurate results as the value field is truncated # on this webpage. To get full value we would need to load # another page. However we don't really need this so long as - # there is only one TXT record for the acme chalenge subdomain. + # there is only one TXT record for the acme challenge subdomain. DNSvalue="$(echo "$line" | cut -d ',' -f 4 | sed 's/^[^"]*"//;s/".*//;s/<\/td>.*//')" if [ $found != 0 ]; then break @@ -192,11 +192,11 @@ dns_freedns_rm() { # Need to read cookie from conf file again in case new value set # during login to FreeDNS when TXT record was created. - # acme.sh does not have a _readaccountconf() fuction + # acme.sh does not have a _readaccountconf() function FREEDNS_COOKIE="$(_read_conf "$ACCOUNT_CONF_PATH" "FREEDNS_COOKIE")" _debug "FreeDNS login cookies: $FREEDNS_COOKIE" - # Sometimes FreeDNS does not reurn the subdomain page but rather + # Sometimes FreeDNS does not return the subdomain page but rather # returns a page regarding becoming a premium member. This usually # happens after a period of inactivity. Immediately trying again # returns the correct subdomain page. So, we will try twice to @@ -302,12 +302,12 @@ _freedns_retrieve_subdomain_page() { export _H2="Accept-Language:en-US" url="https://freedns.afraid.org/subdomain/" - _debug "Retrieve subdmoain page from FreeDNS" + _debug "Retrieve subdomain page from FreeDNS" htmlpage="$(_get "$url")" if [ "$?" != "0" ]; then - _err "FreeDNS retrieve subdomins failed bad RC from _get" + _err "FreeDNS retrieve subdomains failed bad RC from _get" return 1 elif [ -z "$htmlpage" ]; then _err "FreeDNS returned empty subdomain page" @@ -341,7 +341,7 @@ _freedns_add_txt_record() { return 1 elif _contains "$htmlpage" "security code was incorrect"; then _debug "$htmlpage" - _err "FreeDNS failed to add TXT record for $subdomain as FreeDNS requested seurity code" + _err "FreeDNS failed to add TXT record for $subdomain as FreeDNS requested security code" _err "Note that you cannot use automatic DNS validation for FreeDNS public domains" return 1 fi diff --git a/dnsapi/dns_gandi_livedns.sh b/dnsapi/dns_gandi_livedns.sh index 41f42980..28b8f99d 100755 --- a/dnsapi/dns_gandi_livedns.sh +++ b/dnsapi/dns_gandi_livedns.sh @@ -19,7 +19,7 @@ dns_gandi_livedns_add() { txtvalue=$2 if [ -z "$GANDI_LIVEDNS_KEY" ]; then - _err "No API key specifed for Gandi LiveDNS." + _err "No API key specified for Gandi LiveDNS." _err "Create your key and export it as GANDI_LIVEDNS_KEY" return 1 fi diff --git a/dnsapi/dns_infoblox.sh b/dnsapi/dns_infoblox.sh new file mode 100644 index 00000000..3846e62e --- /dev/null +++ b/dnsapi/dns_infoblox.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env sh + +## Infoblox API integration by Jason Keller and Elijah Tenai +## +## Report any bugs via https://github.com/jasonkeller/acme.sh + +dns_infoblox_add() { + + ## Nothing to see here, just some housekeeping + fulldomain=$1 + txtvalue=$2 + baseurlnObject="https://$Infoblox_Server/wapi/v2.2.2/record:txt?name=$fulldomain&text=$txtvalue" + + _info "Using Infoblox API" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + ## Check for the credentials + if [ -z "$Infoblox_Creds" ] || [ -z "$Infoblox_Server" ]; then + Infoblox_Creds="" + Infoblox_Server="" + _err "You didn't specify the credentials or server yet (Infoblox_Creds and Infoblox_Server)." + _err "Please set them via EXPORT ([username:password] and [ip or hostname]) and try again." + return 1 + fi + + ## Save the credentials to the account file + _saveaccountconf Infoblox_Creds "$Infoblox_Creds" + _saveaccountconf Infoblox_Server "$Infoblox_Server" + + ## Base64 encode the credentials + Infoblox_CredsEncoded=$(printf "%b" "$Infoblox_Creds" | _base64) + + ## Construct the HTTP Authorization header + export _H1="Accept-Language:en-US" + export _H2="Authorization: Basic $Infoblox_CredsEncoded" + + ## Add the challenge record to the Infoblox grid member + result=$(_post "" "$baseurlnObject" "" "POST") + + ## Let's see if we get something intelligible back from the unit + if echo "$result" | egrep 'record:txt/.*:.*/default'; then + _info "Successfully created the txt record" + return 0 + else + _err "Error encountered during record addition" + _err "$result" + return 1 + fi + +} + +dns_infoblox_rm() { + + ## Nothing to see here, just some housekeeping + fulldomain=$1 + txtvalue=$2 + + _info "Using Infoblox API" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + ## Base64 encode the credentials + Infoblox_CredsEncoded=$(printf "%b" "$Infoblox_Creds" | _base64) + + ## Construct the HTTP Authorization header + export _H1="Accept-Language:en-US" + export _H2="Authorization: Basic $Infoblox_CredsEncoded" + + ## Does the record exist? Let's check. + baseurlnObject="https://$Infoblox_Server/wapi/v2.2.2/record:txt?name=$fulldomain&text=$txtvalue&_return_type=xml-pretty" + result=$(_get "$baseurlnObject") + + ## Let's see if we get something intelligible back from the grid + if echo "$result" | egrep 'record:txt/.*:.*/default'; then + ## Extract the object reference + objRef=$(printf "%b" "$result" | _egrep_o 'record:txt/.*:.*/default') + objRmUrl="https://$Infoblox_Server/wapi/v2.2.2/$objRef" + ## Delete them! All the stale records! + rmResult=$(_post "" "$objRmUrl" "" "DELETE") + ## Let's see if that worked + if echo "$rmResult" | egrep 'record:txt/.*:.*/default'; then + _info "Successfully deleted $objRef" + return 0 + else + _err "Error occurred during txt record delete" + _err "$rmResult" + return 1 + fi + else + _err "Record to delete didn't match an existing record" + _err "$result" + return 1 + fi +} + +#################### Private functions below ################################## diff --git a/dnsapi/dns_knot.sh b/dnsapi/dns_knot.sh new file mode 100644 index 00000000..094a6981 --- /dev/null +++ b/dnsapi/dns_knot.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env sh + +######## Public functions ##################### + +#Usage: dns_knot_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_knot_add() { + fulldomain=$1 + txtvalue=$2 + _checkKey || return 1 + [ -n "${KNOT_SERVER}" ] || KNOT_SERVER="localhost" + # save the dns server and key to the account.conf file. + _saveaccountconf KNOT_SERVER "${KNOT_SERVER}" + _saveaccountconf KNOT_KEY "${KNOT_KEY}" + + if ! _get_root "$fulldomain"; then + _err "Domain does not exist." + return 1 + fi + + _info "Adding ${fulldomain}. 60 TXT \"${txtvalue}\"" + + knsupdate -y "${KNOT_KEY}" <