From c0d6b4908739884a2d9de20cc71107a5134dfc72 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Tue, 19 Dec 2017 03:48:24 -0800 Subject: [PATCH 01/58] Initial Dockerfile --- Dockerfile | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f4e871c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM alpine:3.6 + +WORKDIR /etc/getssl +COPY getssl . + +RUN apk --no-cache --virtual .run-depends add \ + bash \ + curl \ + openssl From b4cd6e7b87b29ff2e223867db62bb42d5c315a60 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Tue, 19 Dec 2017 04:32:56 -0800 Subject: [PATCH 02/58] Basic running Dockerfile --- Dockerfile | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index f4e871c..6fa3fc2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,13 @@ FROM alpine:3.6 -WORKDIR /etc/getssl -COPY getssl . - +ENV WORKING_DIR="/root/getssl" RUN apk --no-cache --virtual .run-depends add \ bash \ curl \ openssl + +COPY getssl /usr/local/bin/getssl + + +ENTRYPOINT [ "/usr/local/bin/getssl", "--nocheck" ] +CMD [ "--help" ] From 6ba1a04667889446280a4479ed821b57649cbed0 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Tue, 19 Dec 2017 04:48:39 -0800 Subject: [PATCH 03/58] .gitignore --- .gitignore | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7ae2d51 --- /dev/null +++ b/.gitignore @@ -0,0 +1,52 @@ +# Created by .ignore support plugin (hsz.mobi) +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + From 69283954064a533855bf91513b170102fb7b2873 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Tue, 19 Dec 2017 05:01:32 -0800 Subject: [PATCH 04/58] Clean up help message Signed-off-by: Dan Schaper --- getssl | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/getssl b/getssl index 7f3713e..4335725 100755 --- a/getssl +++ b/getssl @@ -930,24 +930,21 @@ graceful_exit() { # normal exit function. help_message() { # print out the help message cat <<- _EOF_ - $PROGNAME ver. $VERSION Obtain SSL certificates from the letsencrypt.org ACME server $(usage) Options: - -a, --all Check all certificates - -d, --debug Outputs debug information - -c, --create Create default config files - -f, --force Force renewal of cert (overrides expiry checks) - -h, --help Display this help message and exit - -q, --quiet Quiet mode (only outputs on error, success of new cert, or getssl was upgraded) - -Q, --mute Like -q, but mutes notification about successful upgrade - -r, --revoke "cert" "key" [CA_server] Revoke a certificate (the cert and key are required) - -u, --upgrade Upgrade getssl if a more recent version is available - -k, --keep "#" Maximum amount of old getssl versions to keep when upgrading - -U, --nocheck Do not check if a more recent version is available - -w working_dir "Working directory" + -a, --all Check all certificates + -d, --debug Output debug information + -c, --create DOMAIN Create default configuration files + -f, --force Force renewal of cert (overrides expiry checks) + -h, --help Display this help message and exit + -q, --quiet Quiet mode (only outputs on error, success of new cert, + or getssl was upgraded) + -Q, --mute Like -q, but mutes notification about successful upgrade + -r, --revoke CERT KEY [CA SERVER] Revoke a certificate + -k, --keep NUMBER Maximum amount of old getssl versions to keep when upgrading _EOF_ } From 4b196d21a40e710f39a45077bc2a478d6d6e27dd Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Tue, 19 Dec 2017 06:06:15 -0800 Subject: [PATCH 05/58] docker-entrypoint scripting --- Dockerfile | 5 +++-- docker-entrypoint.sh | 12 ++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100755 docker-entrypoint.sh diff --git a/Dockerfile b/Dockerfile index 6fa3fc2..b69ef6d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,6 +8,7 @@ RUN apk --no-cache --virtual .run-depends add \ COPY getssl /usr/local/bin/getssl +WORKDIR / -ENTRYPOINT [ "/usr/local/bin/getssl", "--nocheck" ] -CMD [ "--help" ] +COPY ./docker-entrypoint.sh / +ENTRYPOINT [ "/docker-entrypoint.sh" ] diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..cb46df5 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -e + +WORKING_DIR="/getssl" +cd $WORKING_DIR + +if [ "$1" == "" ] && [ ! -f $WORKING_DIR/getssl.cfg ]; then + echo "Type to initialize configuration files." +fi + +getssl --nocheck -w $WORKING_DIR "$@" + From 666c5c9ae960df8e4bd9270e3cdf326f44317065 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Tue, 19 Dec 2017 16:16:22 -0800 Subject: [PATCH 06/58] No need for ENV, doesn't pass through to shell. --- Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b69ef6d..f4bce09 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,5 @@ FROM alpine:3.6 -ENV WORKING_DIR="/root/getssl" RUN apk --no-cache --virtual .run-depends add \ bash \ curl \ From 220757129cbfdcf9fcc625daa2f068fd5a32babd Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Tue, 19 Dec 2017 16:24:54 -0800 Subject: [PATCH 07/58] Remove update options from Usage and Help. Run in Docker does not upgrade. Signed-off-by: Dan Schaper --- getssl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/getssl b/getssl index 4335725..654d8d6 100755 --- a/getssl +++ b/getssl @@ -940,7 +940,7 @@ help_message() { # print out the help message -c, --create DOMAIN Create default configuration files -f, --force Force renewal of cert (overrides expiry checks) -h, --help Display this help message and exit - -q, --quiet Quiet mode (only outputs on error, success of new cert, + -q, --quiet Quiet mode (only outputs on error or success of new cert, or getssl was upgraded) -Q, --mute Like -q, but mutes notification about successful upgrade -r, --revoke CERT KEY [CA SERVER] Revoke a certificate @@ -1268,7 +1268,7 @@ urlbase64() { # urlbase64: base64 encoded string with '+' replaced with '-' and usage() { # echos out the program usage echo "Usage: $PROGNAME [-h|--help] [-d|--debug] [-c|--create] [-f|--force] [-a|--all] [-q|--quiet]"\ - "[-Q|--mute] [-u|--upgrade] [-k|--keep #] [-U|--nocheck] [-r|--revoke cert key] [-w working_dir] domain" + "[-Q|--mute] [-k|--keep #] [-r|--revoke cert key] [-w working_dir] DOMAIN" } write_domain_template() { # write out a template file for a domain. From 6545ffd288be3a208d082e51e8d4fce31f4f7ef5 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Tue, 19 Dec 2017 16:54:38 -0800 Subject: [PATCH 08/58] Remove update options from Usage and Help. Run in Docker does not upgrade. Signed-off-by: Dan Schaper --- getssl | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/getssl b/getssl index 654d8d6..f98832b 100755 --- a/getssl +++ b/getssl @@ -938,13 +938,11 @@ help_message() { # print out the help message -a, --all Check all certificates -d, --debug Output debug information -c, --create DOMAIN Create default configuration files - -f, --force Force renewal of cert (overrides expiry checks) + -f, --force Force renewal of cert - override expiry checks -h, --help Display this help message and exit - -q, --quiet Quiet mode (only outputs on error or success of new cert, - or getssl was upgraded) - -Q, --mute Like -q, but mutes notification about successful upgrade + -q, --quiet Quiet mode - only outputs on error or success of new cert -r, --revoke CERT KEY [CA SERVER] Revoke a certificate - -k, --keep NUMBER Maximum amount of old getssl versions to keep when upgrading + -k, --keep NUMBER Maximum number of old getssl versions to keep when upgrading _EOF_ } @@ -1268,7 +1266,7 @@ urlbase64() { # urlbase64: base64 encoded string with '+' replaced with '-' and usage() { # echos out the program usage echo "Usage: $PROGNAME [-h|--help] [-d|--debug] [-c|--create] [-f|--force] [-a|--all] [-q|--quiet]"\ - "[-Q|--mute] [-k|--keep #] [-r|--revoke cert key] [-w working_dir] DOMAIN" + "[-k|--keep #] [-r|--revoke cert key] [-w working_dir] DOMAIN" } write_domain_template() { # write out a template file for a domain. From d85e245bf3c03da6159627b5274a694d51bd0ba7 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Tue, 19 Dec 2017 16:58:08 -0800 Subject: [PATCH 09/58] No need to keep prior versions of the script in Docker. Working directory is also static for volume/bind mounting. Signed-off-by: Dan Schaper --- getssl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/getssl b/getssl index f98832b..f7893ab 100755 --- a/getssl +++ b/getssl @@ -942,7 +942,6 @@ help_message() { # print out the help message -h, --help Display this help message and exit -q, --quiet Quiet mode - only outputs on error or success of new cert -r, --revoke CERT KEY [CA SERVER] Revoke a certificate - -k, --keep NUMBER Maximum number of old getssl versions to keep when upgrading _EOF_ } @@ -1266,7 +1265,7 @@ urlbase64() { # urlbase64: base64 encoded string with '+' replaced with '-' and usage() { # echos out the program usage echo "Usage: $PROGNAME [-h|--help] [-d|--debug] [-c|--create] [-f|--force] [-a|--all] [-q|--quiet]"\ - "[-k|--keep #] [-r|--revoke cert key] [-w working_dir] DOMAIN" + "[-r|--revoke cert key] DOMAIN" } write_domain_template() { # write out a template file for a domain. From 92f32a9fb0ab857f39a1f920c6e79b38399df777 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Tue, 19 Dec 2017 05:01:32 -0800 Subject: [PATCH 10/58] Clean up help message Signed-off-by: Dan Schaper --- getssl | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/getssl b/getssl index 7f3713e..4335725 100755 --- a/getssl +++ b/getssl @@ -930,24 +930,21 @@ graceful_exit() { # normal exit function. help_message() { # print out the help message cat <<- _EOF_ - $PROGNAME ver. $VERSION Obtain SSL certificates from the letsencrypt.org ACME server $(usage) Options: - -a, --all Check all certificates - -d, --debug Outputs debug information - -c, --create Create default config files - -f, --force Force renewal of cert (overrides expiry checks) - -h, --help Display this help message and exit - -q, --quiet Quiet mode (only outputs on error, success of new cert, or getssl was upgraded) - -Q, --mute Like -q, but mutes notification about successful upgrade - -r, --revoke "cert" "key" [CA_server] Revoke a certificate (the cert and key are required) - -u, --upgrade Upgrade getssl if a more recent version is available - -k, --keep "#" Maximum amount of old getssl versions to keep when upgrading - -U, --nocheck Do not check if a more recent version is available - -w working_dir "Working directory" + -a, --all Check all certificates + -d, --debug Output debug information + -c, --create DOMAIN Create default configuration files + -f, --force Force renewal of cert (overrides expiry checks) + -h, --help Display this help message and exit + -q, --quiet Quiet mode (only outputs on error, success of new cert, + or getssl was upgraded) + -Q, --mute Like -q, but mutes notification about successful upgrade + -r, --revoke CERT KEY [CA SERVER] Revoke a certificate + -k, --keep NUMBER Maximum amount of old getssl versions to keep when upgrading _EOF_ } From 7afd93f7072cb54acb98f57a77b30e4d370b7696 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Tue, 19 Dec 2017 18:49:08 -0800 Subject: [PATCH 11/58] Allow for variables to be set in the environment. Consideration for Docker Compose tool. Signed-off-by: Dan Schaper --- getssl | 98 ++++++++++++++++++++++++++++++---------------------------- 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/getssl b/getssl index 4335725..3c4e98e 100755 --- a/getssl +++ b/getssl @@ -1,6 +1,8 @@ #!/usr/bin/env bash # --------------------------------------------------------------------------- -# getssl - Obtain SSL certificates from the letsencrypt.org ACME server +# getsslD - Obtain SSL certificates from the letsencrypt.org ACME server +# Running in a Docker conatainer. +# Based on the work of https://github.com/srvrco/getssl # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,7 +15,7 @@ # GNU General Public License at for # more details. -# For usage, run "getssl -h" or see https://github.com/srvrco/getssl +# For usage, run "getssl -h" or see # Revision history: # 2016-01-08 Created (v0.1) @@ -189,53 +191,53 @@ PROGNAME=${0##*/} VERSION="2.10" -# defaults -ACCOUNT_KEY_LENGTH=4096 -ACCOUNT_KEY_TYPE="rsa" -CA="https://acme-staging.api.letsencrypt.org" -CA_CERT_LOCATION="" -CHALLENGE_CHECK_TYPE="http" -CHECK_ALL_AUTH_DNS="false" -CHECK_REMOTE="true" -CHECK_REMOTE_WAIT=0 -CODE_LOCATION="https://raw.githubusercontent.com/srvrco/getssl/master/getssl" -CSR_SUBJECT="/" -DEACTIVATE_AUTH="false" -DEFAULT_REVOKE_CA="https://acme-v01.api.letsencrypt.org" -DNS_EXTRA_WAIT="" -DNS_WAIT=10 -DOMAIN_KEY_LENGTH=4096 -DUAL_RSA_ECDSA="false" -GETSSL_IGNORE_CP_PRESERVE="false" -HTTP_TOKEN_CHECK_WAIT=0 -IGNORE_DIRECTORY_DOMAIN="false" +# Default values, accepts environment variables if set, otherwise default are used +ACCOUNT_KEY_LENGTH=${ACCOUNT_KEY_LENGTH:-"4096"} +ACCOUNT_KEY_TYPE=${ACCOUNT_KEY_TYPE:-"rsa"} +CA=${CA:-"https://acme-staging.api.letsencrypt.org"} +CA_CERT_LOCATION=${CA_CERT_LOCATION:-""} +CHALLENGE_CHECK_TYPE=${CHALLENGE_CHECK_TYPE:-"http"} +CHECK_ALL_AUTH_DNS=${CHECK_ALL_AUTH_DNS:-"false"} +CHECK_REMOTE=${CHECK_REMOTE:-"true"} +CHECK_REMOTE_WAIT=${CHECK_REMOTE_WAIT:-"0"} +CODE_LOCATION=${CODE_LOCATION:-"https://raw.githubusercontent.com/dschaper/getssl/master/getssl"} +CSR_SUBJECT=${CSR_SUBJECT:-"/"} +DEACTIVATE_AUTH=${DEACTIVATE_AUTH:-"false"} +DEFAULT_REVOKE_CA=${DEFAULT_REVOKE_CA:-"https://acme-v01.api.letsencrypt.org"} +DNS_EXTRA_WAIT=${DNS_EXTRA_WAIT:-""} +DNS_WAIT=${DNS_WAIT:-"10"} +DOMAIN_KEY_LENGTH=${DOMAIN_KEY_LENGTH:-"4096"} +DUAL_RSA_ECDSA=${DUAL_RSA_ECDSA:-"false"} +GETSSL_IGNORE_CP_PRESERVE=${GETSSL_IGNORE_CP_PRESERVE:-"false"} +HTTP_TOKEN_CHECK_WAIT=${HTTP_TOKEN_CHECK_WAIT:-"0"} +IGNORE_DIRECTORY_DOMAIN=${IGNORE_DIRECTORY_DOMAIN:-"false"} ORIG_UMASK=$(umask) -PREVIOUSLY_VALIDATED="true" -PRIVATE_KEY_ALG="rsa" -PUBLIC_DNS_SERVER="" -RELOAD_CMD="" -RENEW_ALLOW="30" -REUSE_PRIVATE_KEY="true" -SERVER_TYPE="https" -SKIP_HTTP_TOKEN_CHECK="false" -SSLCONF="$(openssl version -d 2>/dev/null| cut -d\" -f2)/openssl.cnf" -OCSP_MUST_STAPLE="false" -TEMP_UPGRADE_FILE="" -TOKEN_USER_ID="" -USE_SINGLE_ACL="false" -VALIDATE_VIA_DNS="" -WORKING_DIR=~/.getssl -_CHECK_ALL=0 -_CREATE_CONFIG=0 -_FORCE_RENEW=0 -_KEEP_VERSIONS="" -_MUTE=0 -_QUIET=0 -_RECREATE_CSR=0 -_REVOKE=0 -_UPGRADE=0 -_UPGRADE_CHECK=1 -_USE_DEBUG=0 +PREVIOUSLY_VALIDATED=${PREVIOUSLY_VALIDATED:-"true"} +PRIVATE_KEY_ALG=${PRIVATE_KEY_ALG:-"rsa"} +PUBLIC_DNS_SERVER=${PUBLIC_DNS_SERVER:-""} +RELOAD_CMD=${RELOAD_CMD:-""} +RENEW_ALLOW=${RENEW_ALLOW:-"30"} +REUSE_PRIVATE_KEY=${REUSE_PRIVATE_KEY:-"true"} +SERVER_TYPE=${SERVER_TYPE:-"https"} +SKIP_HTTP_TOKEN_CHECK=${SKIP_HTTP_TOKEN_CHECK:-"false"} +SSLCONF=${SSLCONF:-"$(openssl version -d 2>/dev/null| cut -d\" -f2)/openssl.cnf"} +OCSP_MUST_STAPLE=${OCSP_MUST_STAPLE:-"false"} +TEMP_UPGRADE_FILE=${TEMP_UPGRADE_FILE:-""} +TOKEN_USER_ID=${TOKEN_USER_ID:-""} +USE_SINGLE_ACL=${USE_SINGLE_ACL:-"false"} +VALIDATE_VIA_DNS=${VALIDATE_VIA_DNS:-""} +WORKING_DIR=${WORKING_DIR:-~/.getssl} +_CHECK_ALL=${_CHECK_ALL:-"0"} +_CREATE_CONFIG=${_CREATE_CONFIG:-"0"} +_FORCE_RENEW=${_FORCE_RENEW:-"0"} +_KEEP_VERSIONS=${_KEEP_VERSIONS:-""} +_MUTE=${MUTE:-"0"} +_QUIET=${_QUIET:-"0"} +_RECREATE_CSR=${_RECREATE_CSR:-"0"} +_REVOKE=${_REVOKE:-"0"} +_UPGRADE=${_UPGRADE:-"0"} +_UPGRADE_CHECK=${_UPGRADE_CHECK:-"1"} +_USE_DEBUG=${_USE_DEBUG:-"0"} config_errors="false" LANG=C From 8cbe7b9d84fb6a11358d86659889d24d506a0b2c Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Tue, 19 Dec 2017 20:38:56 -0800 Subject: [PATCH 12/58] Rename to getsslD and strip out internal upgrade process. Internal upgrades don't work for docker. Signed-off-by: Dan Schaper --- Dockerfile | 2 +- Makefile | 28 ---- README.md | 10 +- docker-entrypoint.sh | 6 +- getssl => getsslD | 306 +++++-------------------------------------- 5 files changed, 39 insertions(+), 313 deletions(-) delete mode 100644 Makefile rename getssl => getsslD (80%) diff --git a/Dockerfile b/Dockerfile index f4bce09..6f77f17 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ RUN apk --no-cache --virtual .run-depends add \ curl \ openssl -COPY getssl /usr/local/bin/getssl +COPY getsslD /usr/local/bin/getsslD WORKDIR / diff --git a/Makefile b/Makefile deleted file mode 100644 index 4f16126..0000000 --- a/Makefile +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (c) 2016 Karol Babioch -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -install: - -ifneq ($(strip $(DESTDIR)),) - mkdir -p $(DESTDIR) -endif - - install -Dm755 getssl $(DESTDIR)/usr/bin/getssl - - install -dm755 $(DESTDIR)/usr/share/getssl - cp -r *_scripts $(DESTDIR)/usr/share/getssl - -.PHONY: install - diff --git a/README.md b/README.md index 879d8f3..fe41861 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# getssl +# getsslD Obtain SSL certificates from the letsencrypt.org ACME server. Suitable for automating the process on remote servers. ## Features @@ -17,7 +17,7 @@ Obtain SSL certificates from the letsencrypt.org ACME server. Suitable for auto ## Installation Since the script is only one file, you can use the following command for a quick installation of GetSSL only: ``` -curl --silent https://raw.githubusercontent.com/srvrco/getssl/master/getssl > getssl ; chmod 700 getssl +curl --silent https://raw.githubusercontent.com/dschaper/getsslD/master/getsslD > getsslD ; chmod 700 getssl ``` This will copy the getssl Bash script to the current location and change the permissions to make it executable for you. @@ -26,13 +26,9 @@ For a more comprehensive installation (e.g. install also helper scripts) use the You'll find the latest version in the git repository: ``` -git clone https://github.com/srvrco/getssl.git +git clone https://github.com/dschaper/getsslD.git ``` -For Arch Linux there are packages in the AUR, see [here](https://aur.archlinux.org/packages/getssl/) and [there](https://aur.archlinux.org/packages/getssl-git/). - -If you use puppet, there is a [GetSSL Puppet module](https://github.com/dthielking/puppet_getssl) by dthielking - ## Overview GetSSL was written in standard bash ( so it can be run on a server, a desktop computer, or even a virtualbox) and add the checks, and certificates to a remote server ( providing you have a ssh with key, sftp or ftp access to the remote server). diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index cb46df5..fbbd060 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash set -e -WORKING_DIR="/getssl" +WORKING_DIR="/getsslD" cd $WORKING_DIR -if [ "$1" == "" ] && [ ! -f $WORKING_DIR/getssl.cfg ]; then - echo "Type to initialize configuration files." +if [ "$1" == "" ] && [ ! -f $WORKING_DIR/getsslD.cfg ]; then + echo "Type to initialize configuration files." fi getssl --nocheck -w $WORKING_DIR "$@" diff --git a/getssl b/getsslD similarity index 80% rename from getssl rename to getsslD index 3c20db7..1c0e212 100755 --- a/getssl +++ b/getsslD @@ -1,7 +1,7 @@ #!/usr/bin/env bash # --------------------------------------------------------------------------- -# getsslD - Obtain SSL certificates from the letsencrypt.org ACME server -# Running in a Docker conatainer. +# getsslD - Obtain SSL certificates from the letsencrypt.org ACME server. +# Runs in a Docker conatainer. # Based on the work of https://github.com/srvrco/getssl # This program is free software: you can redistribute it and/or modify @@ -15,181 +15,10 @@ # GNU General Public License at for # more details. -# For usage, run "getssl -h" or see - -# Revision history: -# 2016-01-08 Created (v0.1) -# 2016-01-11 type correction and upload to github (v0.2) -# 2016-01-11 added import of any existing cert on -c option (v0.3) -# 2016-01-12 corrected formatting of imported certificate (v0.4) -# 2016-01-12 corrected error on removal of token in some instances (v0.5) -# 2016-01-18 corrected issue with removing tmp if run as root with the -c option (v0.6) -# 2016-01-18 added option to upload a single PEN file ( used by cpanel) (v0.7) -# 2016-01-23 added dns challenge option (v0.8) -# 2016-01-24 create the ACL directory if it does not exist. (v0.9) - dstosberg -# 2016-01-26 correcting a couple of small bugs and allow curl to follow redirects (v0.10) -# 2016-01-27 add a very basic openssl.cnf file if it doesn't exist and tidy code slightly (v0.11) -# 2016-01-28 Typo corrections, quoted file variables and fix bug on DNS_DEL_COMMAND (v0.12) -# 2016-01-28 changed DNS checks to use nslookup and allow hyphen in domain names (v0.13) -# 2016-01-29 Fix ssh-reload-command, extra waiting for DNS-challenge, -# 2016-01-29 add error_exit and cleanup help message (v0.14) -# 2016-01-29 added -a|--all option to renew all configured certificates (v0.15) -# 2016-01-29 added option for elliptic curve keys (v0.16) -# 2016-01-29 added server-type option to use and check cert validity from website (v0.17) -# 2016-01-30 added --quiet option for running in cron (v0.18) -# 2016-01-31 removed usage of xxd to make script more compatible across versions (v0.19) -# 2016-01-31 removed usage of base64 to make script more compatible across platforms (v0.20) -# 2016-01-31 added option to safe a full chain certificate (v0.21) -# 2016-02-01 commented code and added option for copying concatenated certs to file (v0.22) -# 2016-02-01 re-arrange flow for DNS-challenge, to reduce time taken (v0.23) -# 2016-02-04 added options for other server types (ldaps, or any port) and check_remote (v0.24) -# 2016-02-04 added short sleep following service restart before checking certs (v0.25) -# 2016-02-12 fix challenge token location when directory doesn't exist (v0.26) -# 2016-02-17 fix sed -E issue, and reduce length of renew check to 365 days for older systems (v0.27) -# 2016-04-05 Ensure DNS cleanup on error exit. (0.28) - pecigonzalo -# 2016-04-15 Remove NS Lookup of A record when using dns validation (0.29) - pecigonzalo -# 2016-04-17 Improving the wording in a couple of comments and info statements. (0.30) -# 2016-05-04 Improve check for if DNS_DEL_COMMAND is blank. (0.31) -# 2016-05-06 Setting umask to 077 for security of private keys etc. (0.32) -# 2016-05-20 update to reflect changes in staging ACME server json (0.33) -# 2016-05-20 tidying up checking of json following AMCE changes. (0.34) -# 2016-05-21 added AUTH_DNS_SERVER to getssl.cfg as optional definition of authoritative DNS server (0.35) -# 2016-05-21 added DNS_WAIT to getssl.cfg as (default = 10 seconds as before) (0.36) -# 2016-05-21 added PUBLIC_DNS_SERVER option, for forcing use of an external DNS server (0.37) -# 2016-05-28 added FTP method of uploading tokens to remote server (blocked for certs as not secure) (0.38) -# 2016-05-28 added FTP method into the default config notes. (0.39) -# 2016-05-30 Add sftp with password to copy files (0.40) -# 2016-05-30 Add version check to see if there is a more recent version of getssl (0.41) -# 2016-05-30 Add [-u|--upgrade] option to automatically upgrade getssl (0.42) -# 2016-05-30 Added backup when auto-upgrading (0.43) -# 2016-05-30 Improvements to auto-upgrade (0.44) -# 2016-05-31 Improved comments - no structural changes -# 2016-05-31 After running for nearly 6 months, final testing prior to a 1.00 stable version. (0.90) -# 2016-06-01 Reorder functions alphabetically as part of code tidy. (0.91) -# 2016-06-03 Version 1.0 of code for release (1.00) -# 2016-06-09 bugfix of issue 44, and add success statement (ignoring quiet flag) (1.01) -# 2016-06-13 test return status of DNS_ADD_COMMAND and error_exit if a problem (hadleyrich) (1.02) -# 2016-06-13 bugfix of issue 45, problem with SERVER_TYPE when it's just a port number (1.03) -# 2016-06-13 bugfix issue 47 - DNS_DEL_COMMAND cleanup was run when not required. (1.04) -# 2016-06-15 add error checking on RELOAD_CMD (1.05) -# 2016-06-20 updated sed and date functions to run on MAC OS X (1.06) -# 2016-06-20 added CHALLENGE_CHECK_TYPE variable to allow checks direct on https rather than http (1.07) -# 2016-06-21 updated grep functions to run on MAC OS X (1.08) -# 2016-06-11 updated to enable running on windows with cygwin (1.09) -# 2016-07-02 Corrections to work with older slackware issue #56 (1.10) -# 2016-07-02 Updating help info re ACL in config file (1.11) -# 2016-07-04 adding DOMAIN_STORAGE as a variable to solve for issue #59 (1.12) -# 2016-07-05 updated order to better handle non-standard DOMAIN_STORAGE location (1.13) -# 2016-07-06 added additional comments about SANS in example template (1.14) -# 2016-07-07 check for duplicate domains in domain / SANS (1.15) -# 2016-07-08 modified to be used on older bash for issue #64 (1.16) -# 2016-07-11 added -w to -a option and comments in domain template (1.17) -# 2016-07-18 remove / regenerate csr when generating new private domain key (1.18) -# 2016-07-21 add output of combined private key and domain cert (1.19) -# 2016-07-21 updated typo (1.20) -# 2016-07-22 corrected issue in nslookup debug option - issue #74 (1.21) -# 2016-07-26 add more server-types based on openssl s_client (1.22) -# 2016-08-01 updated agreement for letsencrypt (1.23) -# 2016-08-02 updated agreement for letsencrypt to update automatically (1.24) -# 2016-08-03 improve messages on test of certificate installation (1.25) -# 2016-08-04 remove carriage return from agreement - issue #80 (1.26) -# 2016-08-04 set permissions for token folders - issue #81 (1.27) -# 2016-08-07 allow default chained file creation - issue #85 (1.28) -# 2016-08-07 use copy rather than move when archiving certs - issue #86 (1.29) -# 2016-08-07 enable use of a single ACL for all checks (if USE_SINGLE_ACL="true" (1.30) -# 2016-08-23 check for already validated domains (issue #93) - (1.31) -# 2016-08-23 updated already validated domains (1.32) -# 2016-08-23 included better force_renew and template for USE_SINGLE_ACL (1.33) -# 2016-08-23 enable insecure certificate on https token check #94 (1.34) -# 2016-08-23 export OPENSSL_CONF so it's used by all openssl commands (1.35) -# 2016-08-25 updated defaults for ACME agreement (1.36) -# 2016-09-04 correct issue #101 when some domains already validated (1.37) -# 2016-09-12 Checks if which is installed (1.38) -# 2016-09-13 Don't check for updates, if -U parameter has been given (1.39) -# 2016-09-17 Improved error messages from invalid certs (1.40) -# 2016-09-19 remove update check on recursive calls when using -a (1.41) -# 2016-09-21 changed shebang for portability (1.42) -# 2016-09-21 Included option to Deactivate an Authorization (1.43) -# 2016-09-22 retry on 500 error from ACME server (1.44) -# 2016-09-22 added additional checks and retry on 500 error from ACME server (1.45) -# 2016-09-24 merged in IPv6 support (1.46) -# 2016-09-27 added additional debug info issue #119 (1.47) -# 2016-09-27 removed IPv6 switch in favour of checking both IPv4 and IPv6 (1.48) -# 2016-09-28 Add -Q, or --mute, switch to mute notifications about successfully upgrading getssl (1.49) -# 2016-09-30 improved portability to work natively on FreeBSD, Slackware and OSX (1.50) -# 2016-09-30 comment out PRIVATE_KEY_ALG from the domain template Issue #125 (1.51) -# 2016-10-03 check remote certificate for right domain before saving to local (1.52) -# 2016-10-04 allow existing CSR with domain name in subject (1.53) -# 2016-10-05 improved the check for CSR with domain in subject (1.54) -# 2016-10-06 prints update info on what was included in latest updates (1.55) -# 2016-10-06 when using -a flag, ignore folders in working directory which aren't domains (1.56) -# 2016-10-12 alllow multiple tokens in DNS challenge (1.57) -# 2016-10-14 added CHECK_ALL_AUTH_DNS option to check all DNS servres, not just one primary server (1.58) -# 2016-10-14 added archive of chain and private key for each cert, and purge old archives (1.59) -# 2016-10-17 updated info comment on failed cert due to rate limits. (1.60) -# 2016-10-17 fix error messages when using 1.0.1e-fips (1.61) -# 2016-10-20 set secure permissions when generating account key (1.62) -# 2016-10-20 set permsissions to 700 for getssl script during upgrade (1.63) -# 2016-10-20 add option to revoke a certificate (1.64) -# 2016-10-21 set revocation server default to acme-v01.api.letsencrypt.org (1.65) -# 2016-10-21 bug fix for revocation on different servers. (1.66) -# 2016-10-22 Tidy up archive code for certificates and reduce permissions for security -# 2016-10-22 Add EC signing for secp384r1 and secp521r1 (the latter not yet supported by Let's Encrypt -# 2016-10-22 Add option to create a new private key for every cert (REUSE_PRIVATE_KEY="true" by default) -# 2016-10-22 Combine EC signing, Private key reuse and archive permissions (1.67) -# 2016-10-25 added CHECK_REMOTE_WAIT option ( to pause before final remote check) -# 2016-10-25 Added EC account key support ( prime256v1, secp384r1 ) (1.68) -# 2016-10-25 Ignore DNS_EXTRA_WAIT if all domains already validated (issue #146) (1.69) -# 2016-10-25 Add option for dual ESA / EDSA certs (1.70) -# 2016-10-25 bug fix Issue #141 challenge error 400 (1.71) -# 2016-10-26 check content of key files, not just recreate if missing. -# 2016-10-26 Improvements on portability (1.72) -# 2016-10-26 Date formatting for busybox (1.73) -# 2016-10-27 bug fix - issue #157 not recognising EC keys on some versions of openssl (1.74) -# 2016-10-31 generate EC account keys and tidy code. -# 2016-10-31 fix warning message if cert doesn't exist (1.75) -# 2016-10-31 remove only specified DNS token #161 (1.76) -# 2016-11-03 Reduce long lines, and remove echo from update (1.77) -# 2016-11-05 added TOKEN_USER_ID (to set ownership of token files ) -# 2016-11-05 updated style to work with latest shellcheck (1.78) -# 2016-11-07 style updates -# 2016-11-07 bug fix DOMAIN_PEM_LOCATION starting with ./ #167 -# 2016-11-08 Fix for openssl 1.1.0 #166 (1.79) -# 2016-11-08 Add and comment optional sshuserid for ssh ACL (1.80) -# 2016-11-09 Add SKIP_HTTP_TOKEN_CHECK option (Issue #170) (1.81) -# 2016-11-13 bug fix DOMAIN_KEY_CERT generation (1.82) -# 2016-11-17 add PREVENT_NON_INTERACTIVE_RENEWAL option (1.83) -# 2016-12-03 add HTTP_TOKEN_CHECK_WAIT option (1.84) -# 2016-12-03 bugfix CSR renewal when no SANS and when using MINGW (1.85) -# 2016-12-16 create CSR_SUBJECT variable - Issue #193 -# 2016-12-16 added fullchain to archive (1.86) -# 2016-12-16 updated DOMAIN_PEM_LOCATION when using DUAL_RSA_ECDSA (1.87) -# 2016-12-19 allow user to ignore permission preservation with nfsv3 shares (1.88) -# 2016-12-19 bug fix for CA (1.89) -# 2016-12-19 included IGNORE_DIRECTORY_DOMAIN option (1.90) -# 2016-12-22 allow copying files to multiple locations (1.91) -# 2016-12-22 bug fix for copying tokens to multiple locations (1.92) -# 2016-12-23 tidy code - place default variables in alphabetical order. -# 2016-12-27 update checks to work with openssl in FIPS mode (1.93) -# 2016-12-28 fix leftover tmpfiles in upgrade routine (1.94) -# 2016-12-28 tidied up upgrade tmpfile handling (1.95) -# 2017-01-01 update comments -# 2017-01-01 create stable release 2.0 (2.00) -# 2017-01-02 Added option to limit amount of old versions to keep (2.01) -# 2017-01-03 Created check_config function to list all obvious config issues (2.02) -# 2017-01-10 force renew if FORCE_RENEWAL file exists (2.03) -# 2017-01-12 added drill, dig or host as alternatives to nslookup (2.04) -# 2017-01-18 bugfix issue #227 - error deleting csr if doesn't exist -# 2017-01-18 issue #228 check private key and account key are different (2.05) -# 2017-01-21 issue #231 mingw bugfix and typos in debug messages (2.06) -# 2017-01-29 issue #232 use neutral locale for date formatting (2.07) -# 2017-01-30 issue #243 compatibility with bash 3.0 (2.08) -# 2017-01-30 issue #243 additional compatibility with bash 3.0 (2.09) -# 2017-02-18 add OCSP Must-Staple to the domain csr generation (2.10) -# ---------------------------------------------------------------------------------------- - -PROGNAME=${0##*/} -VERSION="2.10" +# For usage, run "getsslD -h" or see + +PROGNAME=getsslD +VERSION="1.0" # Default values, accepts environment variables if set, otherwise default are used ACCOUNT_KEY_LENGTH=${ACCOUNT_KEY_LENGTH:-"4096"} @@ -200,7 +29,6 @@ CHALLENGE_CHECK_TYPE=${CHALLENGE_CHECK_TYPE:-"http"} CHECK_ALL_AUTH_DNS=${CHECK_ALL_AUTH_DNS:-"false"} CHECK_REMOTE=${CHECK_REMOTE:-"true"} CHECK_REMOTE_WAIT=${CHECK_REMOTE_WAIT:-"0"} -CODE_LOCATION=${CODE_LOCATION:-"https://raw.githubusercontent.com/dschaper/getssl/master/getssl"} CSR_SUBJECT=${CSR_SUBJECT:-"/"} DEACTIVATE_AUTH=${DEACTIVATE_AUTH:-"false"} DEFAULT_REVOKE_CA=${DEFAULT_REVOKE_CA:-"https://acme-v01.api.letsencrypt.org"} @@ -208,7 +36,7 @@ DNS_EXTRA_WAIT=${DNS_EXTRA_WAIT:-""} DNS_WAIT=${DNS_WAIT:-"10"} DOMAIN_KEY_LENGTH=${DOMAIN_KEY_LENGTH:-"4096"} DUAL_RSA_ECDSA=${DUAL_RSA_ECDSA:-"false"} -GETSSL_IGNORE_CP_PRESERVE=${GETSSL_IGNORE_CP_PRESERVE:-"false"} +GETSSLD_IGNORE_CP_PRESERVE=${GETSSLD_IGNORE_CP_PRESERVE:-"false"} HTTP_TOKEN_CHECK_WAIT=${HTTP_TOKEN_CHECK_WAIT:-"0"} IGNORE_DIRECTORY_DOMAIN=${IGNORE_DIRECTORY_DOMAIN:-"false"} ORIG_UMASK=$(umask) @@ -222,28 +50,22 @@ SERVER_TYPE=${SERVER_TYPE:-"https"} SKIP_HTTP_TOKEN_CHECK=${SKIP_HTTP_TOKEN_CHECK:-"false"} SSLCONF=${SSLCONF:-"$(openssl version -d 2>/dev/null| cut -d\" -f2)/openssl.cnf"} OCSP_MUST_STAPLE=${OCSP_MUST_STAPLE:-"false"} -TEMP_UPGRADE_FILE=${TEMP_UPGRADE_FILE:-""} TOKEN_USER_ID=${TOKEN_USER_ID:-""} USE_SINGLE_ACL=${USE_SINGLE_ACL:-"false"} VALIDATE_VIA_DNS=${VALIDATE_VIA_DNS:-""} -WORKING_DIR=${WORKING_DIR:-~/.getssl} +WORKING_DIR=${WORKING_DIR:-~/.getsslD} _CHECK_ALL=${_CHECK_ALL:-"0"} _CREATE_CONFIG=${_CREATE_CONFIG:-"0"} _FORCE_RENEW=${_FORCE_RENEW:-"0"} -_KEEP_VERSIONS=${_KEEP_VERSIONS:-""} -_MUTE=${MUTE:-"0"} +_MUTE=${_MUTE:-"0"} _QUIET=${_QUIET:-"0"} _RECREATE_CSR=${_RECREATE_CSR:-"0"} _REVOKE=${_REVOKE:-"0"} -_UPGRADE=${_UPGRADE:-"0"} _UPGRADE_CHECK=${_UPGRADE_CHECK:-"1"} _USE_DEBUG=${_USE_DEBUG:-"0"} config_errors="false" LANG=C -# store copy of original command in case of upgrading script and re-running -ORIGCMD="$0 $*" - # Define all functions (in alphabetical order) cert_archive() { # Archive certificate file by copying files to dated archive dir. @@ -264,7 +86,7 @@ cert_archive() { # Archive certificate file by copying files to dated archive d cat "${CERT_FILE::-4}.ec.crt" "${CA_CERT::-4}.ec.crt" > "${DOMAIN_DIR}/archive/${date_time}/fullchain.ec.crt" fi umask "$ORIG_UMASK" - debug "purging old GetSSL archives" + debug "purging old GetSSLD archives" purge_archive "$DOMAIN_DIR" } @@ -387,7 +209,7 @@ check_config() { # check the config files for all obvious errors if [[ $VALIDATE_VIA_DNS != "true" ]]; then # using http-01 challenge if [[ -z "${DOMAIN_ACL}" ]]; then - info "${DOMAIN}: ACL location not specified for domain $d in $DOMAIN_DIR/getssl.cfg" + info "${DOMAIN}: ACL location not specified for domain $d in $DOMAIN_DIR/getsslD.cfg" config_errors=true fi # check domain exist @@ -426,63 +248,6 @@ check_config() { # check the config files for all obvious errors debug "${DOMAIN}: check_config completed - all OK" } -check_getssl_upgrade() { # check if a more recent version of code is available available - TEMP_UPGRADE_FILE="$(mktemp)" - curl --silent "$CODE_LOCATION" --output "$TEMP_UPGRADE_FILE" - errcode=$? - if [[ $errcode -eq 60 ]]; then - error_exit "curl needs updating, your version does not support SNI (multiple SSL domains on a single IP)" - elif [[ $errcode -gt 0 ]]; then - error_exit "curl error : $errcode" - fi - latestversion=$(awk -F '"' '$1 == "VERSION=" {print $2}' "$TEMP_UPGRADE_FILE") - latestvdec=$(echo "$latestversion"| tr -d '.') - localvdec=$(echo "$VERSION"| tr -d '.' ) - debug "current code is version ${VERSION}" - debug "Most recent version is ${latestversion}" - # use a default of 0 for cases where the latest code has not been obtained. - if [[ "${latestvdec:-0}" -gt "$localvdec" ]]; then - if [[ ${_UPGRADE} -eq 1 ]]; then - install "$0" "${0}.v${VERSION}" - install -m 700 "$TEMP_UPGRADE_FILE" "$0" - if [[ ${_MUTE} -eq 0 ]]; then - echo "Updated getssl from v${VERSION} to v${latestversion}" - echo "these update notification can be turned off using the -Q option" - echo "" - echo "Updates are;" - awk "/\(${VERSION}\)$/ {s=1} s; /\(${latestversion}\)$/ {s=0}" "$TEMP_UPGRADE_FILE" | awk '{if(NR>1)print}' - echo "" - fi - if [[ -n "$_KEEP_VERSIONS" ]] && [[ "$_KEEP_VERSIONS" =~ ^[0-9]+$ ]]; then - # Obtain all locally stored old versions in getssl_versions - declare -a getssl_versions - shopt -s nullglob - for getssl_version in $0.v*; do - getssl_versions[${#getssl_versions[@]}]="$getssl_version" - done - shopt -u nullglob - # Explicitly sort the getssl_versions array to make sure - shopt -s -o noglob - IFS=$'\n' getssl_versions=($(sort <<< "${getssl_versions[*]}")) - shopt -u -o noglob - # Remove entries until given amount of old versions to keep is reached - while [[ ${#getssl_versions[@]} -gt $_KEEP_VERSIONS ]]; do - debug "removing old version ${getssl_versions[0]}" - rm "${getssl_versions[0]}" - getssl_versions=("${getssl_versions[@]:1}") - done - fi - eval "$ORIGCMD" - graceful_exit - else - info "" - info "A more recent version (v${latestversion}) of getssl is available, please update" - info "the easiest way is to use the -u or --upgrade flag" - info "" - fi - fi -} - clean_up() { # Perform pre-exit housekeeping umask "$ORIG_UMASK" if [[ $VALIDATE_VIA_DNS == "true" ]]; then @@ -499,9 +264,6 @@ clean_up() { # Perform pre-exit housekeeping if [[ ! -z "$DOMAIN_DIR" ]]; then rm -rf "${TEMP_DIR:?}" fi - if [[ ! -z "$TEMP_UPGRADE_FILE" ]] && [[ -f "$TEMP_UPGRADE_FILE" ]]; then - rm -f "$TEMP_UPGRADE_FILE" - fi } copy_file_to_location() { # copies a file, using scp, sftp or ftp if required. @@ -570,7 +332,7 @@ copy_file_to_location() { # copies a file, using scp, sftp or ftp if required. if ! mkdir -p "$(dirname "$to")" ; then error_exit "cannot create ACL directory $(basename "$to")" fi - if [[ "$GETSSL_IGNORE_CP_PRESERVE" == "true" ]]; then + if [[ "$GETSSLD_IGNORE_CP_PRESERVE" == "true" ]]; then if ! cp "$from" "$to" ; then error_exit "cannot copy $from to $to" fi @@ -1273,8 +1035,8 @@ usage() { # echos out the program usage write_domain_template() { # write out a template file for a domain. cat > "$1" <<- _EOF_domain_ # Uncomment and modify any variables you need - # see https://github.com/srvrco/getssl/wiki/Config-variables for details - # see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs + # see https://github.com/dschaper/getsslD/wiki/Config-variables for details + # see https://github.com/dschaper/getsslD/wiki/Example-config-files for example configs # # The staging server is best for testing #CA="https://acme-staging.api.letsencrypt.org" @@ -1323,10 +1085,10 @@ write_domain_template() { # write out a template file for a domain. _EOF_domain_ } -write_getssl_template() { # write out the main template file - cat > "$1" <<- _EOF_getssl_ +write_getsslD_template() { # write out the main template file + cat > "$1" <<- _EOF_getsslD_ # Uncomment and modify any variables you need - # see https://github.com/srvrco/getssl/wiki/Config-variables for details + # see https://github.com/dschaper/getsslD/wiki/Config-variables for details # # The staging server is best for testing (hence set as default) CA="https://acme-staging.api.letsencrypt.org" @@ -1359,7 +1121,7 @@ write_getssl_template() { # write out the main template file #VALIDATE_VIA_DNS="true" #DNS_ADD_COMMAND= #DNS_DEL_COMMAND= - _EOF_getssl_ + _EOF_getsslD_ } write_openssl_conf() { # write out a minimal openssl conf @@ -1389,8 +1151,6 @@ while [[ -n ${1+defined} ]]; do _FORCE_RENEW=1 ;; -a | --all) _CHECK_ALL=1 ;; - -k | --keep) - shift; _KEEP_VERSIONS="$1";; -q | --quiet) _QUIET=1 ;; -Q | --mute) @@ -1404,8 +1164,6 @@ while [[ -n ${1+defined} ]]; do REVOKE_KEY="$1" shift REVOKE_CA="$1" ;; - -u | --upgrade) - _UPGRADE=1 ;; -U | --nocheck) _UPGRADE_CHECK=0 ;; -w) @@ -1452,7 +1210,7 @@ requires mktemp # Check if upgrades are available (unless they have specified -U to ignore Upgrade checks) if [[ $_UPGRADE_CHECK -eq 1 ]]; then - check_getssl_upgrade + check_getsslD_upgrade fi # Revoke a certificate if requested @@ -1486,10 +1244,10 @@ if [[ ! -d "$WORKING_DIR" ]]; then fi # read any variables from config in working directory -if [[ -s "$WORKING_DIR/getssl.cfg" ]]; then - debug "reading config from $WORKING_DIR/getssl.cfg" +if [[ -s "$WORKING_DIR/getsslD.cfg" ]]; then + debug "reading config from $WORKING_DIR/getsslD.cfg" # shellcheck source=/dev/null - . "$WORKING_DIR/getssl.cfg" + . "$WORKING_DIR/getsslD.cfg" fi # Define defaults for variables not set in the main config. @@ -1548,23 +1306,23 @@ fi # if "-c|--create" option used, then create config files. if [[ ${_CREATE_CONFIG} -eq 1 ]]; then # If main config file does not exists then create it. - if [[ ! -s "$WORKING_DIR/getssl.cfg" ]]; then - info "creating main config file $WORKING_DIR/getssl.cfg" + if [[ ! -s "$WORKING_DIR/getsslD.cfg" ]]; then + info "creating main config file $WORKING_DIR/getsslD.cfg" if [[ ! -s "$SSLCONF" ]]; then SSLCONF="$WORKING_DIR/openssl.cnf" write_openssl_conf "$SSLCONF" fi - write_getssl_template "$WORKING_DIR/getssl.cfg" + write_getsslD_template "$WORKING_DIR/getsslD.cfg" fi # If domain and domain config don't exist then create them. if [[ ! -d "$DOMAIN_DIR" ]]; then info "Making domain directory - $DOMAIN_DIR" mkdir -p "$DOMAIN_DIR" fi - if [[ -s "$DOMAIN_DIR/getssl.cfg" ]]; then - info "domain config already exists $DOMAIN_DIR/getssl.cfg" + if [[ -s "$DOMAIN_DIR/getsslD.cfg" ]]; then + info "domain config already exists $DOMAIN_DIR/getsslD.cfg" else - info "creating domain config file in $DOMAIN_DIR/getssl.cfg" + info "creating domain config file in $DOMAIN_DIR/getsslD.cfg" # if domain has an existing cert, copy from domain and use to create defaults. EX_CERT=$(echo \ | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:443" 2>/dev/null \ @@ -1576,7 +1334,7 @@ if [[ ${_CREATE_CONFIG} -eq 1 ]]; then | grep -Eo "DNS:[a-zA-Z 0-9.-]*" | sed "s@DNS:$DOMAIN@@g" | grep -v '^$' | cut -c 5-) EX_SANS=${EX_SANS//$'\n'/','} fi - write_domain_template "$DOMAIN_DIR/getssl.cfg" + write_domain_template "$DOMAIN_DIR/getsslD.cfg" fi TEMP_DIR="$DOMAIN_DIR/tmp" # end of "-c|--create" option, so exit @@ -1598,10 +1356,10 @@ if [[ ! -d "${TEMP_DIR}" ]]; then fi # read any variables from config in domain directory -if [[ -s "$DOMAIN_DIR/getssl.cfg" ]]; then - debug "reading config from $DOMAIN_DIR/getssl.cfg" +if [[ -s "$DOMAIN_DIR/getsslD.cfg" ]]; then + debug "reading config from $DOMAIN_DIR/getsslD.cfg" # shellcheck source=/dev/null - . "$DOMAIN_DIR/getssl.cfg" + . "$DOMAIN_DIR/getsslD.cfg" fi # from SERVER_TYPE set REMOTE_PORT and REMOTE_EXTRA From b381605271198b7ed2fbc860518833432222dd1f Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Tue, 19 Dec 2017 20:49:57 -0800 Subject: [PATCH 13/58] Rename to getsslD and strip out internal upgrade process. Internal upgrades don't work for docker. Signed-off-by: Dan Schaper --- docker-entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index fbbd060..b1f998a 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -8,5 +8,5 @@ if [ "$1" == "" ] && [ ! -f $WORKING_DIR/getsslD.cfg ]; then echo "Type to initialize configuration files." fi -getssl --nocheck -w $WORKING_DIR "$@" +getsslD --nocheck -w $WORKING_DIR "$@" From cbde566c40093150400af1ed7bfffbfc2cca8ad7 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Tue, 19 Dec 2017 21:30:36 -0800 Subject: [PATCH 14/58] OS: Remove bsd code OS: Remove linux and cygwin OS: Remove os OS: Remove mac OS: Remove unknown os Remove OS detection. Alpine in Docker will always run busybox. Signed-off-by: Dan Schaper --- getsslD | 79 ++++----------------------------------------------------- 1 file changed, 5 insertions(+), 74 deletions(-) diff --git a/getsslD b/getsslD index 1c0e212..77a9382 100755 --- a/getsslD +++ b/getsslD @@ -424,27 +424,11 @@ create_key() { # create a domain key (if it doesn't already exist) } date_epoc() { # convert the date into epoch time - if [[ "$os" == "bsd" ]]; then - date -j -f "%b %d %T %Y %Z" "$1" +%s - elif [[ "$os" == "mac" ]]; then - date -j -f "%b %d %T %Y %Z" "$1" +%s - elif [[ "$os" == "busybox" ]]; then - de_ld=$(echo "$1" | awk '{print $1 $2 $3 $4}') - date -D "%b %d %T %Y" -d "$de_ld" +%s - else - date -d "$1" +%s - fi - + date -D "%b %d %T %Y" -d "$(echo "$1" | awk '{print $1 $2 $3 $4}')" +%s } date_fmt() { # format date from epoc time to YYYY-MM-DD - if [[ "$os" == "bsd" ]]; then #uses older style date function. - date -j -f "%s" "$1" +%F - elif [[ "$os" == "mac" ]]; then # MAC OSX uses older BSD style date. - date -j -f "%s" "$1" +%F - else - date -d "@$1" +%F - fi + date -d "@$1" +%F } date_renew() { # calculates the renewal time in epoch @@ -469,17 +453,6 @@ get_auth_dns() { # get the authoritative dns server for a domain (sets primary_n gad_d="$1" # domain name gad_s="$PUBLIC_DNS_SERVER" # start with PUBLIC_DNS_SERVER - if [[ "$os" == "cygwin" ]]; then - all_auth_dns_servers=$(nslookup -type=soa "${d}" ${PUBLIC_DNS_SERVER} 2>/dev/null \ - | grep "primary name server" \ - | awk '{print $NF}') - if [[ -z "$all_auth_dns_servers" ]]; then - error_exit "couldn't find primary DNS server - please set AUTH_DNS_SERVER in config" - fi - primary_ns="$all_auth_dns_servers" - return - fi - if [[ "$DNS_CHECK_FUNC" == "drill" ]] || [[ "$DNS_CHECK_FUNC" == "dig" ]]; then if [[ -z "$gad_s" ]]; then #checking for CNAMEs res=$($DNS_CHECK_FUNC CNAME "$gad_d"| grep "^$gad_d") @@ -613,26 +586,6 @@ get_cr() { # get curl response return $ret } -get_os() { # function to get the current Operating System - uname_res=$(uname -s) - if [[ $(date -h 2>&1 | grep -ic busybox) -gt 0 ]]; then - os="busybox" - elif [[ ${uname_res} == "Linux" ]]; then - os="linux" - elif [[ ${uname_res} == "FreeBSD" ]]; then - os="bsd" - elif [[ ${uname_res} == "Darwin" ]]; then - os="mac" - elif [[ ${uname_res:0:6} == "CYGWIN" ]]; then - os="cygwin" - elif [[ ${uname_res:0:5} == "MINGW" ]]; then - os="mingw" - else - os="unknown" - fi - debug "detected os type = $os" -} - get_signing_params() { # get signing parameters from key skey=$1 if openssl rsa -in "${skey}" -noout 2>/dev/null ; then # RSA key @@ -747,13 +700,7 @@ json_get() { # get the value corresponding to $2 in the JSON passed as $1. } os_esed() { # Use different sed version for different os types (extended regex) - if [[ "$os" == "bsd" ]]; then # BSD requires -E flag for extended regex - sed -E "${@}" - elif [[ "$os" == "mac" ]]; then # MAC uses older BSD style sed. - sed -E "${@}" - else - sed -r "${@}" - fi + sed -r "${@}" } purge_archive() { # purge archive of old, invalid, certificates @@ -763,13 +710,7 @@ purge_archive() { # purge archive of old, invalid, certificates # check each directory if [[ -d "$padir" ]]; then tstamp=$(basename "$padir"| awk -F"_" '{print $1"-"$2"-"$3" "$4":"$5}') - if [[ "$os" == "bsd" ]]; then - direpoc=$(date -j -f "%F %H:%M" "$tstamp" +%s) - elif [[ "$os" == "mac" ]]; then - direpoc=$(date -j -f "%F %H:%M" "$tstamp" +%s) - else - direpoc=$(date -d "$tstamp" +%s) - fi + direpoc=$(date -d "$tstamp" +%s) current_epoc=$(date "+%s") # as certs currently valid for 90 days, purge anything older than 100 purgedate=$((current_epoc - 60*60*24*100)) @@ -1186,9 +1127,6 @@ done # Main logic ############ -# Get the current OS, so the correct functions can be used for that OS. (sets the variable os) -get_os - # check if "recent" version of bash. #if [[ "${BASH_VERSINFO[0]}${BASH_VERSINFO[1]}" -lt 42 ]]; then # info "this script is designed for bash v4.2 or later - earlier version may give errors" @@ -1257,9 +1195,6 @@ DOMAIN_DIR="$DOMAIN_STORAGE/$DOMAIN" CERT_FILE="$DOMAIN_DIR/${DOMAIN}.crt" CA_CERT="$DOMAIN_DIR/chain.crt" TEMP_DIR="$DOMAIN_DIR/tmp" -if [[ "$os" == "mingw" ]]; then - CSR_SUBJECT="//" -fi # Set the OPENSSL_CONF environment variable so openssl knows which config to use export OPENSSL_CONF=$SSLCONF @@ -1729,11 +1664,7 @@ if [[ $VALIDATE_VIA_DNS == "true" ]]; then ntries=0 check_dns="fail" while [[ "$check_dns" == "fail" ]]; do - if [[ "$os" == "cygwin" ]]; then - check_result=$(nslookup -type=txt "_acme-challenge.${d}" "${ns}" \ - | grep ^_acme -A2\ - | grep '"'|awk -F'"' '{ print $2}') - elif [[ "$DNS_CHECK_FUNC" == "drill" ]] || [[ "$DNS_CHECK_FUNC" == "dig" ]]; then + if [[ "$DNS_CHECK_FUNC" == "drill" ]] || [[ "$DNS_CHECK_FUNC" == "dig" ]]; then check_result=$($DNS_CHECK_FUNC TXT "_acme-challenge.${d}" "@${ns}" \ | grep ^_acme|awk -F'"' '{ print $2}') elif [[ "$DNS_CHECK_FUNC" == "host" ]]; then From 298b012d5f24472563ad5185282a9348e40a0c53 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Wed, 20 Dec 2017 05:23:01 -0800 Subject: [PATCH 15/58] Remove OS specific `sed` function. Hardcode `sed` in to calling functions. Signed-off-by: Dan Schaper --- getsslD | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/getsslD b/getsslD index 77a9382..6c19e4a 100755 --- a/getsslD +++ b/getsslD @@ -664,8 +664,7 @@ help_message() { # print out the help message } hex2bin() { # Remove spaces, add leading zero, escape as hex string ensuring no trailing new line char -# printf -- "$(cat | os_esed -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')" - echo -e -n "$(cat | os_esed -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')" + echo -e -n "$(cat | sed -r -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')" } info() { # write out info as long as the quiet flag has not been set. @@ -699,10 +698,6 @@ json_get() { # get the value corresponding to $2 in the JSON passed as $1. fi } -os_esed() { # Use different sed version for different os types (extended regex) - sed -r "${@}" -} - purge_archive() { # purge archive of old, invalid, certificates arcdir="$1/archive" debug "purging archives in ${arcdir}/" @@ -965,7 +960,7 @@ signal_exit() { # Handle trapped signals } urlbase64() { # urlbase64: base64 encoded string with '+' replaced with '-' and '/' replaced with '_' - openssl base64 -e | tr -d '\n\r' | os_esed -e 's:=*$::g' -e 'y:+/:-_:' + openssl base64 -e | tr -d '\n\r' | sed -r -e 's:=*$::g' -e 'y:+/:-_:' } usage() { # echos out the program usage From 0293837c3e975c3c26d9c08e78be47017a62bd9f Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Wed, 20 Dec 2017 06:25:53 -0800 Subject: [PATCH 16/58] `cert_archive` cleanup. Signed-off-by: Dan Schaper --- getsslD | 55 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 17 deletions(-) diff --git a/getsslD b/getsslD index 6c19e4a..f6a3e19 100755 --- a/getsslD +++ b/getsslD @@ -32,6 +32,7 @@ CHECK_REMOTE_WAIT=${CHECK_REMOTE_WAIT:-"0"} CSR_SUBJECT=${CSR_SUBJECT:-"/"} DEACTIVATE_AUTH=${DEACTIVATE_AUTH:-"false"} DEFAULT_REVOKE_CA=${DEFAULT_REVOKE_CA:-"https://acme-v01.api.letsencrypt.org"} +DEFAULT_UMASK=${DEFAULT_UMASK:-"u=rx,g=rx,o="} DNS_EXTRA_WAIT=${DNS_EXTRA_WAIT:-""} DNS_WAIT=${DNS_WAIT:-"10"} DOMAIN_KEY_LENGTH=${DOMAIN_KEY_LENGTH:-"4096"} @@ -68,25 +69,45 @@ LANG=C # Define all functions (in alphabetical order) -cert_archive() { # Archive certificate file by copying files to dated archive dir. - debug "creating an achive copy of current new certs" - date_time=$(date +%Y_%m_%d_%H_%M) - mkdir -p "${DOMAIN_DIR}/archive/${date_time}" - umask 077 - cp "$CERT_FILE" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.crt" - cp "$DOMAIN_DIR/${DOMAIN}.csr" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.csr" - cp "$DOMAIN_DIR/${DOMAIN}.key" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.key" - cp "$CA_CERT" "${DOMAIN_DIR}/archive/${date_time}/chain.crt" - cat "$CERT_FILE" "$CA_CERT" > "${DOMAIN_DIR}/archive/${date_time}/fullchain.crt" +cert_archive() { + # Archive certificates files + # Create directory for day, store certs by DOMAIN-YYYY_MM_DD:HH_MM UTC + + debug "Copying generated certs to archive..." + + local date_time + date_time=$(date -u +%Y_%m_%d_%H_%M) + local date + date=$(date -u +Y_%m_%d) + local archive_dir="${DOMAIN_DIR}/archive/${date}" + local archive_suffix="${DOMAIN}-${date_time}" + local orig_umask + orig_umask=$(umask) + + umask "${DEFAULT_UMASK}" + mkdir -p "${archive_dir}" + debug " ${archive_dir} created." + + cp "${CERT_FILE}" "${archive_dir}/${archive_suffix}.crt" + cp "${DOMAIN_DIR}/${DOMAIN}.csr" "${archive_dir}/${archive_suffix}.csr" + cp "${DOMAIN_DIR}/${DOMAIN}.key" "${archive_dir}/${archive_suffix}.key" + cp "${CA_CERT}" "${archive_dir}/${archive_suffix}-chain.crt" + cat "$CERT_FILE" "$CA_CERT" > "${archive_dir}/${archive_suffix}-fullchain.crt" + debug " RSA certs and chains copied." + if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then - cp "${CERT_FILE::-4}.ec.crt" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.ec.crt" - cp "$DOMAIN_DIR/${DOMAIN}.ec.csr" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.ec.csr" - cp "$DOMAIN_DIR/${DOMAIN}.ec.key" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.ec.key" - cp "${CA_CERT::-4}.ec.crt" "${DOMAIN_DIR}/archive/${date_time}/chain.ec.crt" - cat "${CERT_FILE::-4}.ec.crt" "${CA_CERT::-4}.ec.crt" > "${DOMAIN_DIR}/archive/${date_time}/fullchain.ec.crt" + cp "${CERT_FILE::-4}.ec.crt" "${archive_dir}/${archive_suffix}.ec.crt" + cp "$DOMAIN_DIR/${DOMAIN}.ec.csr" "${archive_dir}/${archive_suffix}.ec.csr" + cp "$DOMAIN_DIR/${DOMAIN}.ec.key" "${archive_dir}/${archive_suffix}.ec.key" + cp "${CA_CERT::-4}.ec.crt" "${archive_dir}/${archive_suffix}-chain.ec.crt" + cat "${CERT_FILE::-4}.ec.crt" "${CA_CERT::-4}.ec.crt" > "${archive_dir}/${archive_suffix}-fullchain.ec.crt" fi - umask "$ORIG_UMASK" - debug "purging old GetSSLD archives" + debug " EC certs and chains copied." + + umask "${orig_umask}" + + # Call purge_archive to clear out old files + debug "Purging old getsslD archives" purge_archive "$DOMAIN_DIR" } From d164db527f5c74bed0f712e5456d29a8ba8e960a Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Wed, 20 Dec 2017 06:54:35 -0800 Subject: [PATCH 17/58] Remove required binary checks, dependencies are built in to the image. Signed-off-by: Dan Schaper --- getsslD | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/getsslD b/getsslD index f6a3e19..56810b0 100755 --- a/getsslD +++ b/getsslD @@ -1150,17 +1150,7 @@ done #check if required applications are included -requires which -requires openssl -requires curl requires nslookup drill dig host DNS_CHECK_FUNC -requires awk -requires tr -requires date -requires grep -requires sed -requires sort -requires mktemp # Check if upgrades are available (unless they have specified -U to ignore Upgrade checks) if [[ $_UPGRADE_CHECK -eq 1 ]]; then From 04e9da36eedd8645fc46d8870afa56551694975d Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Wed, 20 Dec 2017 07:00:35 -0800 Subject: [PATCH 18/58] DNS check function uses native nslookup, remove DNS_CHECK_FUNC Signed-off-by: Dan Schaper --- getsslD | 74 ++------------------------------------------------------- 1 file changed, 2 insertions(+), 72 deletions(-) diff --git a/getsslD b/getsslD index 56810b0..3a7d52d 100755 --- a/getsslD +++ b/getsslD @@ -234,21 +234,7 @@ check_config() { # check the config files for all obvious errors config_errors=true fi # check domain exist - if [[ "$DNS_CHECK_FUNC" == "drill" ]] || [[ "$DNS_CHECK_FUNC" == "dig" ]]; then - if [[ "$($DNS_CHECK_FUNC "${d}" SOA|grep -c "^${d}")" -ge 1 ]]; then - debug "found IP for ${d}" - else - info "${DOMAIN}: DNS lookup failed for ${d}" - config_errors=true - fi - elif [[ "$DNS_CHECK_FUNC" == "host" ]]; then - if [[ "$($DNS_CHECK_FUNC "${d}" |grep -c "^${d}")" -ge 1 ]]; then - debug "found IP for ${d}" - else - info "${DOMAIN}: DNS lookup failed for ${d}" - config_errors=true - fi - elif [[ "$(nslookup -query=AAAA "${d}"|grep -c "^${d}.*has AAAA address")" -ge 1 ]]; then + if [[ "$(nslookup -query=AAAA "${d}"|grep -c "^${d}.*has AAAA address")" -ge 1 ]]; then debug "found IPv6 record for ${d}" elif [[ "$(nslookup "${d}"| grep -c ^Name)" -ge 1 ]]; then debug "found IPv4 record for ${d}" @@ -474,52 +460,6 @@ get_auth_dns() { # get the authoritative dns server for a domain (sets primary_n gad_d="$1" # domain name gad_s="$PUBLIC_DNS_SERVER" # start with PUBLIC_DNS_SERVER - if [[ "$DNS_CHECK_FUNC" == "drill" ]] || [[ "$DNS_CHECK_FUNC" == "dig" ]]; then - if [[ -z "$gad_s" ]]; then #checking for CNAMEs - res=$($DNS_CHECK_FUNC CNAME "$gad_d"| grep "^$gad_d") - else - res=$($DNS_CHECK_FUNC CNAME "$gad_d" "@$gad_s"| grep "^$gad_d") - fi - if [[ ! -z "$res" ]]; then # domain is a CNAME so get main domain - gad_d=$(echo "$res"| awk '{print $5}' |sed 's/\.$//g') - fi - if [[ -z "$gad_s" ]]; then #checking for CNAMEs - res=$($DNS_CHECK_FUNC NS "$gad_d"| grep "^$gad_d") - else - res=$($DNS_CHECK_FUNC NS "$gad_d" "@$gad_s"| grep "^$gad_d") - fi - if [[ -z "$res" ]]; then - error_exit "couldn't find primary DNS server - please set AUTH_DNS_SERVER in config" - else - all_auth_dns_servers=$(echo "$res" | awk '$4 ~ "NS" {print $5}' | sed 's/\.$//g'|tr '\n' ' ') - fi - if [[ $CHECK_ALL_AUTH_DNS == "true" ]]; then - primary_ns="$all_auth_dns_servers" - else - primary_ns=$(echo "$all_auth_dns_servers" | awk '{print $1}') - fi - return - fi - - if [[ "$DNS_CHECK_FUNC" == "host" ]]; then - if [[ -z "$gad_s" ]]; then - res=$($DNS_CHECK_FUNC -t NS "$gad_d"| grep "name server") - else - res=$($DNS_CHECK_FUNC -t NS "$gad_d" "$gad_s"| grep "name server") - fi - if [[ -z "$res" ]]; then - error_exit "couldn't find primary DNS server - please set AUTH_DNS_SERVER in config" - else - all_auth_dns_servers=$(echo "$res" | awk '{print $4}' | sed 's/\.$//g'|tr '\n' ' ') - fi - if [[ $CHECK_ALL_AUTH_DNS == "true" ]]; then - primary_ns="$all_auth_dns_servers" - else - primary_ns=$(echo "$all_auth_dns_servers" | awk '{print $1}') - fi - return - fi - res=$(nslookup -debug=1 -type=soa -type=ns "$gad_d" ${gad_s}) if [[ "$(echo "$res" | grep -c "Non-authoritative")" -gt 0 ]]; then @@ -1150,8 +1090,6 @@ done #check if required applications are included -requires nslookup drill dig host DNS_CHECK_FUNC - # Check if upgrades are available (unless they have specified -U to ignore Upgrade checks) if [[ $_UPGRADE_CHECK -eq 1 ]]; then check_getsslD_upgrade @@ -1670,16 +1608,8 @@ if [[ $VALIDATE_VIA_DNS == "true" ]]; then ntries=0 check_dns="fail" while [[ "$check_dns" == "fail" ]]; do - if [[ "$DNS_CHECK_FUNC" == "drill" ]] || [[ "$DNS_CHECK_FUNC" == "dig" ]]; then - check_result=$($DNS_CHECK_FUNC TXT "_acme-challenge.${d}" "@${ns}" \ - | grep ^_acme|awk -F'"' '{ print $2}') - elif [[ "$DNS_CHECK_FUNC" == "host" ]]; then - check_result=$($DNS_CHECK_FUNC -t TXT "_acme-challenge.${d}" "${ns}" \ + check_result=$(nslookup -type=txt "_acme-challenge.${d}" "${ns}" \ | grep ^_acme|awk -F'"' '{ print $2}') - else - check_result=$(nslookup -type=txt "_acme-challenge.${d}" "${ns}" \ - | grep ^_acme|awk -F'"' '{ print $2}') - fi debug "expecting $auth_key" debug "${ns} gave ... $check_result" From 1ebc972053fc7d20f5dd9e897b5a0bafd4f717aa Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Wed, 20 Dec 2017 07:02:51 -0800 Subject: [PATCH 19/58] Remove commented bash version check, required version always included. Signed-off-by: Dan Schaper --- getsslD | 7 ------- 1 file changed, 7 deletions(-) diff --git a/getsslD b/getsslD index 3a7d52d..6883b7f 100755 --- a/getsslD +++ b/getsslD @@ -1083,13 +1083,6 @@ done # Main logic ############ -# check if "recent" version of bash. -#if [[ "${BASH_VERSINFO[0]}${BASH_VERSINFO[1]}" -lt 42 ]]; then -# info "this script is designed for bash v4.2 or later - earlier version may give errors" -#fi - -#check if required applications are included - # Check if upgrades are available (unless they have specified -U to ignore Upgrade checks) if [[ $_UPGRADE_CHECK -eq 1 ]]; then check_getsslD_upgrade From 1f0d3b1a6eefbd5517e11f6c20d8ace21e753bbb Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Wed, 20 Dec 2017 07:19:25 -0800 Subject: [PATCH 20/58] Clean out upgrade option calls and variables. Signed-off-by: Dan Schaper --- getsslD | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/getsslD b/getsslD index 6883b7f..03192ec 100755 --- a/getsslD +++ b/getsslD @@ -62,7 +62,6 @@ _MUTE=${_MUTE:-"0"} _QUIET=${_QUIET:-"0"} _RECREATE_CSR=${_RECREATE_CSR:-"0"} _REVOKE=${_REVOKE:-"0"} -_UPGRADE_CHECK=${_UPGRADE_CHECK:-"1"} _USE_DEBUG=${_USE_DEBUG:-"0"} config_errors="false" LANG=C @@ -1061,8 +1060,6 @@ while [[ -n ${1+defined} ]]; do REVOKE_KEY="$1" shift REVOKE_CA="$1" ;; - -U | --nocheck) - _UPGRADE_CHECK=0 ;; -w) shift; WORKING_DIR="$1" ;; -* | --*) @@ -1083,11 +1080,6 @@ done # Main logic ############ -# Check if upgrades are available (unless they have specified -U to ignore Upgrade checks) -if [[ $_UPGRADE_CHECK -eq 1 ]]; then - check_getsslD_upgrade -fi - # Revoke a certificate if requested if [[ $_REVOKE -eq 1 ]]; then if [[ -z $REVOKE_CA ]]; then @@ -1155,7 +1147,7 @@ if [[ ${_CHECK_ALL} -eq 1 ]]; then for dir in ${DOMAIN_STORAGE}/*; do if [[ -d "$dir" ]]; then debug "Checking $dir" - cmd="$0 -U" # No update checks when calling recursively + cmd="$0" if [[ ${_USE_DEBUG} -eq 1 ]]; then cmd="$cmd -d" fi From 130abe36e438935357163007edf53ccd031745af Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Wed, 20 Dec 2017 08:11:05 -0800 Subject: [PATCH 21/58] Change numeric comparison to string comparison. Always compare against "true". Signed-off-by: Dan Schaper --- getsslD | 68 +++++++++++++++++++++++++++------------------------------ 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/getsslD b/getsslD index 03192ec..85c84d8 100755 --- a/getsslD +++ b/getsslD @@ -55,14 +55,13 @@ TOKEN_USER_ID=${TOKEN_USER_ID:-""} USE_SINGLE_ACL=${USE_SINGLE_ACL:-"false"} VALIDATE_VIA_DNS=${VALIDATE_VIA_DNS:-""} WORKING_DIR=${WORKING_DIR:-~/.getsslD} -_CHECK_ALL=${_CHECK_ALL:-"0"} -_CREATE_CONFIG=${_CREATE_CONFIG:-"0"} -_FORCE_RENEW=${_FORCE_RENEW:-"0"} -_MUTE=${_MUTE:-"0"} -_QUIET=${_QUIET:-"0"} -_RECREATE_CSR=${_RECREATE_CSR:-"0"} -_REVOKE=${_REVOKE:-"0"} -_USE_DEBUG=${_USE_DEBUG:-"0"} +_CHECK_ALL=${_CHECK_ALL:-"false"} +_CREATE_CONFIG=${_CREATE_CONFIG:-"false"} +_FORCE_RENEW=${_FORCE_RENEW:-"false"} +_QUIET=${_QUIET:-"false"} +_RECREATE_CSR=${_RECREATE_CSR:-"false"} +_REVOKE=${_REVOKE:-"false"} +_USE_DEBUG=${_USE_DEBUG:-"false"} config_errors="false" LANG=C @@ -107,7 +106,7 @@ cert_archive() { # Call purge_archive to clear out old files debug "Purging old getsslD archives" - purge_archive "$DOMAIN_DIR" + purge_archive "${DOMAIN_DIR}" } check_challenge_completion() { # checks with the ACME server if our challenge is OK @@ -374,19 +373,19 @@ create_csr() { # create a csr using a given key (if it doesn't already exist) if [[ "$(echo "${domains_in_csr}"| grep "^${d}$")" != "${d}" ]]; then info "existing csr at $csr_file does not contain ${d} - re-create-csr"\ ".... $(echo "${domains_in_csr}"| grep "^${d}$")" - _RECREATE_CSR=1 + _RECREATE_CSR="true" fi done # check all domains in csr are in config if [[ "$alldomains" != "$domains_in_csr" ]]; then info "existing csr at $csr_file does not have the same domains as the config - re-create-csr" - _RECREATE_CSR=1 + _RECREATE_CSR="true" fi fi # end of ... check if domain csr exists - if not then create it # if CSR does not exist, or flag set to recreate, then create csr - if [[ ! -s "$csr_file" ]] || [[ "$_RECREATE_CSR" == "1" ]]; then + if [[ ! -s "$csr_file" ]] || [[ "$_RECREATE_CSR" == "true" ]]; then info "creating domain csr - $csr_file" # create a temporary config file, for portability. tmp_conf=$(mktemp) @@ -443,7 +442,7 @@ date_renew() { # calculates the renewal time in epoch } debug() { # write out debug info if the debug flag has been set - if [[ ${_USE_DEBUG} -eq 1 ]]; then + if [[ ${_USE_DEBUG} == "true" ]]; then echo " " echo "$@" fi @@ -628,7 +627,7 @@ hex2bin() { # Remove spaces, add leading zero, escape as hex string ensuring no } info() { # write out info as long as the quiet flag has not been set. - if [[ ${_QUIET} -eq 0 ]]; then + if [[ ${_QUIET} != "true" ]]; then echo "$@" fi } @@ -790,7 +789,7 @@ send_signed_request() { # Sends a request to the ACME server, signed with your p CURL_HEADER="$TEMP_DIR/curl.header" dp="$TEMP_DIR/curl.dump" CURL="curl --silent --dump-header $CURL_HEADER " - if [[ ${_USE_DEBUG} -eq 1 ]]; then + if [[ ${_USE_DEBUG} == "true" ]]; then CURL="$CURL --trace-ascii $dp " fi @@ -1040,20 +1039,17 @@ while [[ -n ${1+defined} ]]; do -h | --help) help_message; graceful_exit ;; -d | --debug) - _USE_DEBUG=1 ;; + _USE_DEBUG="true" ;; -c | --create) - _CREATE_CONFIG=1 ;; + _CREATE_CONFIG="true" ;; -f | --force) - _FORCE_RENEW=1 ;; + _FORCE_RENEW="true" ;; -a | --all) - _CHECK_ALL=1 ;; + _CHECK_ALL="true" ;; -q | --quiet) - _QUIET=1 ;; - -Q | --mute) - _QUIET=1 - _MUTE=1 ;; + _QUIET="true" ;; -r | --revoke) - _REVOKE=1 + _REVOKE="true" shift REVOKE_CERT="$1" shift @@ -1081,11 +1077,11 @@ done ############ # Revoke a certificate if requested -if [[ $_REVOKE -eq 1 ]]; then +if [[ $_REVOKE == "true" ]]; then if [[ -z $REVOKE_CA ]]; then CA=$DEFAULT_REVOKE_CA elif [[ "$REVOKE_CA" == "-d" ]]; then - _USE_DEBUG=1 + _USE_DEBUG="true" CA=$DEFAULT_REVOKE_CA else CA=$REVOKE_CA @@ -1099,7 +1095,7 @@ fi AGREEMENT=$(curl -I "${CA}/terms" 2>/dev/null | awk '$1 ~ "Location:" {print $2}'|tr -d '\r') # if nothing in command line, print help and exit. -if [[ -z "$DOMAIN" ]] && [[ ${_CHECK_ALL} -ne 1 ]]; then +if [[ -z "$DOMAIN" ]] && [[ ${_CHECK_ALL} != "true" ]]; then help_message graceful_exit fi @@ -1129,14 +1125,14 @@ TEMP_DIR="$DOMAIN_DIR/tmp" export OPENSSL_CONF=$SSLCONF # if "-a" option then check other parameters and create run for each domain. -if [[ ${_CHECK_ALL} -eq 1 ]]; then +if [[ ${_CHECK_ALL} == "true" ]]; then info "Check all certificates" - if [[ ${_CREATE_CONFIG} -eq 1 ]]; then + if [[ ${_CREATE_CONFIG} == "true" ]]; then error_exit "cannot combine -c|--create with -a|--all" fi - if [[ ${_FORCE_RENEW} -eq 1 ]]; then + if [[ ${_FORCE_RENEW} == "true" ]]; then error_exit "cannot combine -f|--force with -a|--all because of rate limits" fi @@ -1148,10 +1144,10 @@ if [[ ${_CHECK_ALL} -eq 1 ]]; then if [[ -d "$dir" ]]; then debug "Checking $dir" cmd="$0" - if [[ ${_USE_DEBUG} -eq 1 ]]; then + if [[ ${_USE_DEBUG} == "true" ]]; then cmd="$cmd -d" fi - if [[ ${_QUIET} -eq 1 ]]; then + if [[ ${_QUIET} == "true" ]]; then cmd="$cmd -q" fi # check if $dir looks like a domain name (contains a period) @@ -1168,7 +1164,7 @@ fi # end of "-a" option (looping through all domains) # if "-c|--create" option used, then create config files. -if [[ ${_CREATE_CONFIG} -eq 1 ]]; then +if [[ ${_CREATE_CONFIG} == "true" ]]; then # If main config file does not exists then create it. if [[ ! -s "$WORKING_DIR/getsslD.cfg" ]]; then info "creating main config file $WORKING_DIR/getsslD.cfg" @@ -1234,7 +1230,7 @@ check_config if [[ -e "$DOMAIN_DIR/FORCE_RENEWAL" ]]; then rm -f "$DOMAIN_DIR/FORCE_RENEWAL" || error_exit "problem deleting file $DOMAIN_DIR/FORCE_RENEWAL" - _FORCE_RENEW=1 + _FORCE_RENEW="true" info "${DOMAIN}: forcing renewal (due to FORCE_RENEWAL file)" fi @@ -1245,7 +1241,7 @@ URL_new_authz=$(echo "$ca_all_loc" | grep "new-authz" | awk -F'"' '{print $4}') URL_new_cert=$(echo "$ca_all_loc" | grep "new-cert" | awk -F'"' '{print $4}') # if check_remote is true then connect and obtain the current certificate (if not forcing renewal) -if [[ "${CHECK_REMOTE}" == "true" ]] && [[ $_FORCE_RENEW -eq 0 ]]; then +if [[ "${CHECK_REMOTE}" == "true" ]] && [[ $_FORCE_RENEW != "true" ]]; then debug "getting certificate for $DOMAIN from remote server" # shellcheck disable=SC2086 EX_CERT=$(echo \ @@ -1324,7 +1320,7 @@ if [[ -s "$CERT_FILE" ]]; then debug "local cert is valid until $enddate" if [[ "$enddate" != "-" ]]; then enddate_s=$(date_epoc "$enddate") - if [[ $(date_renew) -lt "$enddate_s" ]] && [[ $_FORCE_RENEW -ne 1 ]]; then + if [[ $(date_renew) -lt "$enddate_s" ]] && [[ $_FORCE_RENEW != "true" ]]; then issuer=$(openssl x509 -in "$CERT_FILE" -noout -issuer 2>/dev/null) if [[ "$issuer" == *"Fake LE Intermediate"* ]] && [[ "$CA" == "https://acme-v01.api.letsencrypt.org" ]]; then debug "upgradeing from fake cert to real" From 523db8b70adc6549765595dc5f0c7a5c36a70c3d Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Wed, 20 Dec 2017 08:42:00 -0800 Subject: [PATCH 22/58] Adjust for storing script in / and not in path. Signed-off-by: Dan Schaper --- Dockerfile | 3 +-- docker-entrypoint.sh | 6 +++--- getsslD | 9 +++++++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6f77f17..03331a6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,8 +5,7 @@ RUN apk --no-cache --virtual .run-depends add \ curl \ openssl -COPY getsslD /usr/local/bin/getsslD - +COPY getsslD / WORKDIR / COPY ./docker-entrypoint.sh / diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index b1f998a..3439e8d 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,12 +1,12 @@ #!/usr/bin/env bash set -e -WORKING_DIR="/getsslD" +WORKING_DIR="/.getsslD" cd $WORKING_DIR if [ "$1" == "" ] && [ ! -f $WORKING_DIR/getsslD.cfg ]; then - echo "Type to initialize configuration files." + echo "Run with <-c DOMAIN> to initialize configuration files." fi -getsslD --nocheck -w $WORKING_DIR "$@" +/getsslD -w $WORKING_DIR -d "$@" diff --git a/getsslD b/getsslD index 85c84d8..85d3925 100755 --- a/getsslD +++ b/getsslD @@ -454,6 +454,11 @@ error_exit() { # give error message on error exit exit 1 } +error() { + # Write error message to STDERR for log. + echo "$@" >&2 +} + get_auth_dns() { # get the authoritative dns server for a domain (sets primary_ns ) gad_d="$1" # domain name gad_s="$PUBLIC_DNS_SERVER" # start with PUBLIC_DNS_SERVER @@ -923,8 +928,7 @@ urlbase64() { # urlbase64: base64 encoded string with '+' replaced with '-' and } usage() { # echos out the program usage - echo "Usage: $PROGNAME [-h|--help] [-d|--debug] [-c|--create] [-f|--force] [-a|--all] [-q|--quiet]"\ - "[-r|--revoke cert key] DOMAIN" + echo "Usage: $PROGNAME [options] [ARGS]" } write_domain_template() { # write out a template file for a domain. @@ -1096,6 +1100,7 @@ AGREEMENT=$(curl -I "${CA}/terms" 2>/dev/null | awk '$1 ~ "Location:" {print $2} # if nothing in command line, print help and exit. if [[ -z "$DOMAIN" ]] && [[ ${_CHECK_ALL} != "true" ]]; then + error "Domain is required for this option." help_message graceful_exit fi From 63e01f0c5584bf5600f1616fe7fefe96a4f295bf Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Wed, 20 Dec 2017 09:58:50 -0800 Subject: [PATCH 23/58] Further tweaks to cert_archive function. Report status to terminal and remove one extra date call. Signed-off-by: Dan Schaper --- getsslD | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/getsslD b/getsslD index 85d3925..c05f342 100755 --- a/getsslD +++ b/getsslD @@ -71,41 +71,38 @@ cert_archive() { # Archive certificates files # Create directory for day, store certs by DOMAIN-YYYY_MM_DD:HH_MM UTC - debug "Copying generated certs to archive..." + info "Copying generated certs to archive..." local date_time date_time=$(date -u +%Y_%m_%d_%H_%M) - local date - date=$(date -u +Y_%m_%d) + local date=${date_time::10} local archive_dir="${DOMAIN_DIR}/archive/${date}" local archive_suffix="${DOMAIN}-${date_time}" - local orig_umask - orig_umask=$(umask) umask "${DEFAULT_UMASK}" mkdir -p "${archive_dir}" - debug " ${archive_dir} created." + info " ${archive_dir} created." cp "${CERT_FILE}" "${archive_dir}/${archive_suffix}.crt" cp "${DOMAIN_DIR}/${DOMAIN}.csr" "${archive_dir}/${archive_suffix}.csr" cp "${DOMAIN_DIR}/${DOMAIN}.key" "${archive_dir}/${archive_suffix}.key" cp "${CA_CERT}" "${archive_dir}/${archive_suffix}-chain.crt" cat "$CERT_FILE" "$CA_CERT" > "${archive_dir}/${archive_suffix}-fullchain.crt" - debug " RSA certs and chains copied." + info " RSA certs and chains copied." if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then cp "${CERT_FILE::-4}.ec.crt" "${archive_dir}/${archive_suffix}.ec.crt" - cp "$DOMAIN_DIR/${DOMAIN}.ec.csr" "${archive_dir}/${archive_suffix}.ec.csr" - cp "$DOMAIN_DIR/${DOMAIN}.ec.key" "${archive_dir}/${archive_suffix}.ec.key" + cp "${DOMAIN_DIR}/${DOMAIN}.ec.csr" "${archive_dir}/${archive_suffix}.ec.csr" + cp "${DOMAIN_DIR}/${DOMAIN}.ec.key" "${archive_dir}/${archive_suffix}.ec.key" cp "${CA_CERT::-4}.ec.crt" "${archive_dir}/${archive_suffix}-chain.ec.crt" cat "${CERT_FILE::-4}.ec.crt" "${CA_CERT::-4}.ec.crt" > "${archive_dir}/${archive_suffix}-fullchain.ec.crt" fi - debug " EC certs and chains copied." + info " EC certs and chains copied." - umask "${orig_umask}" + umask "${ORIG_UMASK}" # Call purge_archive to clear out old files - debug "Purging old getsslD archives" + info "Purging old getsslD archives" purge_archive "${DOMAIN_DIR}" } From bb192a74ab630d3eae6a30b05033ddf2bed5034f Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Wed, 20 Dec 2017 12:20:09 -0800 Subject: [PATCH 24/58] Add bind-tools to get full `nslookup`, not just busybox `nslookup`. Signed-off-by: Dan Schaper --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 03331a6..d299338 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,7 @@ FROM alpine:3.6 RUN apk --no-cache --virtual .run-depends add \ bash \ + bind-tools \ curl \ openssl From 8aba88febf5d3c6ac9683491ce48ea78ad9de925 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Wed, 20 Dec 2017 18:22:30 -0800 Subject: [PATCH 25/58] Remove `requires` function. Dependencies handled by docker image. Add Neilpang's `acme.sh` as attribution. Set default public DNS server to Google. Modify openssl key generator. Signed-off-by: Dan Schaper --- getsslD | 195 +++++++++++++++++++++++++++----------------------------- 1 file changed, 95 insertions(+), 100 deletions(-) diff --git a/getsslD b/getsslD index c05f342..a713968 100755 --- a/getsslD +++ b/getsslD @@ -2,7 +2,8 @@ # --------------------------------------------------------------------------- # getsslD - Obtain SSL certificates from the letsencrypt.org ACME server. # Runs in a Docker conatainer. -# Based on the work of https://github.com/srvrco/getssl +# Based on the work of getssl by srvrco https://github.com/srvrco/getssl +# and acme.sh by Neil Pang http://Neilpang/acme.sh # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -43,7 +44,7 @@ IGNORE_DIRECTORY_DOMAIN=${IGNORE_DIRECTORY_DOMAIN:-"false"} ORIG_UMASK=$(umask) PREVIOUSLY_VALIDATED=${PREVIOUSLY_VALIDATED:-"true"} PRIVATE_KEY_ALG=${PRIVATE_KEY_ALG:-"rsa"} -PUBLIC_DNS_SERVER=${PUBLIC_DNS_SERVER:-""} +PUBLIC_DNS_SERVER=${PUBLIC_DNS_SERVER:-"8.8.8.8"} RELOAD_CMD=${RELOAD_CMD:-""} RENEW_ALLOW=${RENEW_ALLOW:-"30"} REUSE_PRIVATE_KEY=${REUSE_PRIVATE_KEY:-"true"} @@ -108,7 +109,7 @@ cert_archive() { check_challenge_completion() { # checks with the ACME server if our challenge is OK uri=$1 - domain=$2 + g_domain=$2 keyauthorization=$3 debug "sending request to ACME server saying we're ready for challenge" @@ -116,35 +117,35 @@ check_challenge_completion() { # checks with the ACME server if our challenge is # check response from our request to perform challenge if [[ ! -z "$code" ]] && [[ ! "$code" == '202' ]] ; then - error_exit "$domain:Challenge error: $code" + error_exit "$g_domain:Challenge error: $code" fi # loop "forever" to keep checking for a response from the ACME server. while true ; do debug "checking" if ! get_cr "$uri" ; then - error_exit "$domain:Verify error:$code" + error_exit "$g_domain:Verify error:$code" fi status=$(json_get "$response" status) # If ACME response is valid, then break out of loop if [[ "$status" == "valid" ]] ; then - info "Verified $domain" + info "Verified $g_domain" break; fi # if ACME response is that their check gave an invalid response, error exit if [[ "$status" == "invalid" ]] ; then err_detail=$(json_get "$response" detail) - error_exit "$domain:Verify error:$err_detail" + error_exit "$g_domain:Verify error:$err_detail" fi # if ACME response is pending ( they haven't completed checks yet) then wait and try again. if [[ "$status" == "pending" ]] ; then info "Pending" else - error_exit "$domain:Verify error:$response" + error_exit "$g_domain:Verify error:$response" fi debug "sleep 5 secs before testing verify again" sleep 5 @@ -398,31 +399,43 @@ create_csr() { # create a csr using a given key (if it doesn't already exist) fi } -create_key() { # create a domain key (if it doesn't already exist) - key_type=$1 # domain key type - key_loc=$2 # domain key location - key_len=$3 # domain key length - for rsa keys. - # check if key exists, if not then create it. - if [[ -s "$key_loc" ]]; then - debug "domain key exists at $key_loc - skipping generation" - # ideally need to check validity of domain key +create_key() { + # Create an openSSL key + local key_loc=${1} + local key_len=${2} + local key_type + + # Determine key type by length + # Valid Let's Encrypt RSA key lengths 2048-4096 + # Valid Let's Encrypt ECC key lengths 256, 384, 521*(Not implemented) + + if [[ "${key_len}" -ge 2048 ]] && [[ "${key_len}" -le 4096 ]]; then + key_type="RSA" + elif [[ "${key_len}" -eq 256 ]]; then + key_type="prime256v1" + elif [[ "${key_len}" -eq 384 ]]; then + key_type="secp384r1" + elif [[ "${key_len}" -eq 521 ]]; then + key_type="secp521r1" else - umask 077 - info "creating key - $key_loc" - case "$key_type" in - rsa) - openssl genrsa "$key_len" > "$key_loc";; - prime256v1|secp384r1|secp521r1) - openssl ecparam -genkey -name "$key_type" > "$key_loc";; - *) - error_exit "unknown private key algorithm type $key_loc";; - esac - umask "$ORIG_UMASK" - # remove csr on generation of new domain key - if [[ -e "${key_loc::-4}.csr" ]]; then - rm -f "${key_loc::-4}.csr" - fi + error "Invalid key length. Please check you configuration." + return 1 fi + + case "$key_type" in + RSA) + openssl genrsa -out "${key_loc}" "${key_len}" >& /dev/null + return 0 + ;; + prime256v1|secp384r1|secp521r1) + openssl ecparam -genkey -out "${key_loc}" -name "${key_type}" >& /dev/null + return 0 + ;; + esac + + # Error inside case statement openssl generation + rm "${key_loc}" + return 1 } date_epoc() { # convert the date into epoch time @@ -456,36 +469,38 @@ error() { echo "$@" >&2 } -get_auth_dns() { # get the authoritative dns server for a domain (sets primary_ns ) - gad_d="$1" # domain name - gad_s="$PUBLIC_DNS_SERVER" # start with PUBLIC_DNS_SERVER +get_auth_dns() { + # Find authoritative DNS server for domain via SOA lookup. + local g_domain="$1" + local g_server="$PUBLIC_DNS_SERVER" + local result - res=$(nslookup -debug=1 -type=soa -type=ns "$gad_d" ${gad_s}) + result=$(nslookup -debug=1 -type=soa -type=ns "${g_domain}" "${g_server}") - if [[ "$(echo "$res" | grep -c "Non-authoritative")" -gt 0 ]]; then + if echo "${result}" | grep -q "Non-authoritative"; then # this is a Non-authoritative server, need to check for an authoritative one. - gad_s=$(echo "$res" | awk '$2 ~ "nameserver" {print $4; exit }' |sed 's/\.$//g') - if [[ "$(echo "$res" | grep -c "an't find")" -gt 0 ]]; then + gad_s=$(echo "${result}" | awk '$2 ~ "nameserver" {print $4; exit }' |sed 's/\.$//g') + if [[ "$(echo "${result}" | grep -c "an't find")" -gt 0 ]]; then # if domain name doesn't exist, then find auth servers for next level up - gad_s=$(echo "$res" | awk '$1 ~ "origin" {print $3; exit }') - gad_d=$(echo "$res" | awk '$1 ~ "->" {print $2; exit}') + gad_s=$(echo "${result}" | awk '$1 ~ "origin" {print $3; exit }') + g_domain=$(echo "${result}" | awk '$1 ~ "->" {print $2; exit}') fi fi if [[ -z "$gad_s" ]]; then - res=$(nslookup -debug=1 -type=soa -type=ns "$gad_d") + res=$(nslookup -debug=1 -type=soa -type=ns "${g_domain}") else - res=$(nslookup -debug=1 -type=soa -type=ns "$gad_d" "${gad_s}") + res=$(nslookup -debug=1 -type=soa -type=ns "${g_domain}" "${g_server}") fi if [[ "$(echo "$res" | grep -c "canonical name")" -gt 0 ]]; then - gad_d=$(echo "$res" | awk ' $2 ~ "canonical" {print $5; exit }' |sed 's/\.$//g') + g_domain=$(echo "$res" | awk ' $2 ~ "canonical" {print $5; exit }' |sed 's/\.$//g') elif [[ "$(echo "$res" | grep -c "an't find")" -gt 0 ]]; then gad_s=$(echo "$res" | awk ' $1 ~ "origin" {print $3; exit }') - gad_d=$(echo "$res"| awk '$1 ~ "->" {print $2; exit}') + g_domain=$(echo "$res"| awk '$1 ~ "->" {print $2; exit}') fi - all_auth_dns_servers=$(nslookup -type=soa -type=ns "$gad_d" "$gad_s" \ + all_auth_dns_servers=$(nslookup -type=soa -type=ns "${g_domain}" "${g_server}" \ | awk ' $2 ~ "nameserver" {print $4}' \ | sed 's/\.$//g'| tr '\n' ' ') if [[ $CHECK_ALL_AUTH_DNS == "true" ]]; then @@ -716,29 +731,6 @@ revoke_certificate() { # revoke a certificate fi } -requires() { # check if required function is available - if [[ "$#" -gt 1 ]]; then # if more than 1 value, check list - for i in "$@"; do - if [[ "$i" == "${!#}" ]]; then # if on last variable then exit as not found - error_exit "this script requires one of: ${*:1:$(($#-1))}" - fi - res=$(which "$i" 2>/dev/null) - debug "checking for $i ... $res" - if [[ ! -z "$res" ]]; then # if function found, then set variable to function and return - debug "function $i found at $res - setting ${!#} to $i" - eval "${!#}=\$i" - return - fi - done - else # only one value, so check it. - result=$(which "$1" 2>/dev/null) - debug "checking for required $1 ... $result" - if [[ -z "$result" ]]; then - error_exit "This script requires $1 installed" - fi - fi -} - set_server_type() { # uses SERVER_TYPE to set REMOTE_PORT and REMOTE_EXTRA if [[ ${SERVER_TYPE} == "https" ]] || [[ ${SERVER_TYPE} == "webserver" ]]; then REMOTE_PORT=443 @@ -996,7 +988,7 @@ write_getsslD_template() { # write out the main template file # Set an email address associated with your account - generally set at account level rather than domain. #ACCOUNT_EMAIL="me@example.com" ACCOUNT_KEY_LENGTH=4096 - ACCOUNT_KEY="$WORKING_DIR/account.key" + ACCOUNT_KEY="$WORKING_DIR/account_key.pem" PRIVATE_KEY_ALG="rsa" #REUSE_PRIVATE_KEY="true" @@ -1074,8 +1066,40 @@ while [[ -n ${1+defined} ]]; do shift done +##### # Main logic -############ +##### + +# if the "working directory" doesn't exist, then create it. +if [[ ! -d "$WORKING_DIR" ]]; then + debug "Making working directory - $WORKING_DIR" + mkdir -p "$WORKING_DIR" +fi + +# read any variables from config in working directory +if [[ -s "$WORKING_DIR/getsslD.cfg" ]]; then + debug "reading config from $WORKING_DIR/getsslD.cfg" + # shellcheck source=/dev/null + . "$WORKING_DIR/getsslD.cfg" +fi + +# Define defaults for variables not set in the main config. +ACCOUNT_KEY="${ACCOUNT_KEY:=$WORKING_DIR/account_key.pem}" +DOMAIN_STORAGE="${DOMAIN_STORAGE:=$WORKING_DIR}" +DOMAIN_DIR="$DOMAIN_STORAGE/$DOMAIN" +CERT_FILE="$DOMAIN_DIR/${DOMAIN}.crt" +CA_CERT="$DOMAIN_DIR/chain.crt" +TEMP_DIR="$DOMAIN_DIR/tmp" + + +# create account key if it doesn't exist. +if [[ -s "$ACCOUNT_KEY" ]]; then + info "Account key exists at $ACCOUNT_KEY skipping generation" +else + info "Creating account key $ACCOUNT_KEY" + create_key "$ACCOUNT_KEY" "$ACCOUNT_KEY_LENGTH" +fi + # Revoke a certificate if requested if [[ $_REVOKE == "true" ]]; then @@ -1102,27 +1126,6 @@ if [[ -z "$DOMAIN" ]] && [[ ${_CHECK_ALL} != "true" ]]; then graceful_exit fi -# if the "working directory" doesn't exist, then create it. -if [[ ! -d "$WORKING_DIR" ]]; then - debug "Making working directory - $WORKING_DIR" - mkdir -p "$WORKING_DIR" -fi - -# read any variables from config in working directory -if [[ -s "$WORKING_DIR/getsslD.cfg" ]]; then - debug "reading config from $WORKING_DIR/getsslD.cfg" - # shellcheck source=/dev/null - . "$WORKING_DIR/getsslD.cfg" -fi - -# Define defaults for variables not set in the main config. -ACCOUNT_KEY="${ACCOUNT_KEY:=$WORKING_DIR/account.key}" -DOMAIN_STORAGE="${DOMAIN_STORAGE:=$WORKING_DIR}" -DOMAIN_DIR="$DOMAIN_STORAGE/$DOMAIN" -CERT_FILE="$DOMAIN_DIR/${DOMAIN}.crt" -CA_CERT="$DOMAIN_DIR/chain.crt" -TEMP_DIR="$DOMAIN_DIR/tmp" - # Set the OPENSSL_CONF environment variable so openssl knows which config to use export OPENSSL_CONF=$SSLCONF @@ -1344,14 +1347,6 @@ if [[ ! -t 0 ]] && [[ "$PREVENT_NON_INTERACTIVE_RENEWAL" = "true" ]]; then error_exit "$errmsg" fi -# create account key if it doesn't exist. -if [[ -s "$ACCOUNT_KEY" ]]; then - debug "Account key exists at $ACCOUNT_KEY skipping generation" -else - info "creating account key $ACCOUNT_KEY" - create_key "$ACCOUNT_KEY_TYPE" "$ACCOUNT_KEY" "$ACCOUNT_KEY_LENGTH" -fi - # if not reusing priavte key, then remove the old keys if [[ "$REUSE_PRIVATE_KEY" != "true" ]]; then if [[ -s "$DOMAIN_DIR/${DOMAIN}.key" ]]; then @@ -1739,7 +1734,7 @@ if [[ "$DEACTIVATE_AUTH" == "true" ]]; then if [[ "$code" == "200" ]]; then debug "Authorization deactivated" else - error_exit "$domain: Deactivation error: $code" + error_exit "$g_domain: Deactivation error: $code" fi done fi From 1b1f638b72e7ccce295a45f1f36958055ee50727 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Thu, 21 Dec 2017 22:01:59 -0800 Subject: [PATCH 26/58] Change variable sourcing to replace variables with supplied environment defaults. `:-` becomes `:=`. Signed-off-by: Dan Schaper --- getsslD | 80 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/getsslD b/getsslD index a713968..ab76be3 100755 --- a/getsslD +++ b/getsslD @@ -22,47 +22,47 @@ PROGNAME=getsslD VERSION="1.0" # Default values, accepts environment variables if set, otherwise default are used -ACCOUNT_KEY_LENGTH=${ACCOUNT_KEY_LENGTH:-"4096"} -ACCOUNT_KEY_TYPE=${ACCOUNT_KEY_TYPE:-"rsa"} -CA=${CA:-"https://acme-staging.api.letsencrypt.org"} -CA_CERT_LOCATION=${CA_CERT_LOCATION:-""} -CHALLENGE_CHECK_TYPE=${CHALLENGE_CHECK_TYPE:-"http"} -CHECK_ALL_AUTH_DNS=${CHECK_ALL_AUTH_DNS:-"false"} -CHECK_REMOTE=${CHECK_REMOTE:-"true"} -CHECK_REMOTE_WAIT=${CHECK_REMOTE_WAIT:-"0"} -CSR_SUBJECT=${CSR_SUBJECT:-"/"} -DEACTIVATE_AUTH=${DEACTIVATE_AUTH:-"false"} -DEFAULT_REVOKE_CA=${DEFAULT_REVOKE_CA:-"https://acme-v01.api.letsencrypt.org"} -DEFAULT_UMASK=${DEFAULT_UMASK:-"u=rx,g=rx,o="} -DNS_EXTRA_WAIT=${DNS_EXTRA_WAIT:-""} -DNS_WAIT=${DNS_WAIT:-"10"} -DOMAIN_KEY_LENGTH=${DOMAIN_KEY_LENGTH:-"4096"} -DUAL_RSA_ECDSA=${DUAL_RSA_ECDSA:-"false"} -GETSSLD_IGNORE_CP_PRESERVE=${GETSSLD_IGNORE_CP_PRESERVE:-"false"} -HTTP_TOKEN_CHECK_WAIT=${HTTP_TOKEN_CHECK_WAIT:-"0"} -IGNORE_DIRECTORY_DOMAIN=${IGNORE_DIRECTORY_DOMAIN:-"false"} +ACCOUNT_KEY_LENGTH=${ACCOUNT_KEY_LENGTH:="4096"} +ACCOUNT_KEY_TYPE=${ACCOUNT_KEY_TYPE:="rsa"} +CA=${CA:="https://acme-staging.api.letsencrypt.org"} +CA_CERT_LOCATION=${CA_CERT_LOCATION:=""} +CHALLENGE_CHECK_TYPE=${CHALLENGE_CHECK_TYPE:="http"} +CHECK_ALL_AUTH_DNS=${CHECK_ALL_AUTH_DNS:="false"} +CHECK_REMOTE=${CHECK_REMOTE:="true"} +CHECK_REMOTE_WAIT=${CHECK_REMOTE_WAIT:="0"} +CSR_SUBJECT=${CSR_SUBJECT:="/"} +DEACTIVATE_AUTH=${DEACTIVATE_AUTH:="false"} +DEFAULT_REVOKE_CA=${DEFAULT_REVOKE_CA:="https://acme-v01.api.letsencrypt.org"} +DEFAULT_UMASK=${DEFAULT_UMASK:="u=rx,g=rx,o="} +DNS_EXTRA_WAIT=${DNS_EXTRA_WAIT:=""} +DNS_WAIT=${DNS_WAIT:="10"} +DOMAIN_KEY_LENGTH=${DOMAIN_KEY_LENGTH:="4096"} +DUAL_RSA_ECDSA=${DUAL_RSA_ECDSA:="false"} +GETSSLD_IGNORE_CP_PRESERVE=${GETSSLD_IGNORE_CP_PRESERVE:="false"} +HTTP_TOKEN_CHECK_WAIT=${HTTP_TOKEN_CHECK_WAIT:="0"} +IGNORE_DIRECTORY_DOMAIN=${IGNORE_DIRECTORY_DOMAIN:="false"} ORIG_UMASK=$(umask) -PREVIOUSLY_VALIDATED=${PREVIOUSLY_VALIDATED:-"true"} -PRIVATE_KEY_ALG=${PRIVATE_KEY_ALG:-"rsa"} -PUBLIC_DNS_SERVER=${PUBLIC_DNS_SERVER:-"8.8.8.8"} -RELOAD_CMD=${RELOAD_CMD:-""} -RENEW_ALLOW=${RENEW_ALLOW:-"30"} -REUSE_PRIVATE_KEY=${REUSE_PRIVATE_KEY:-"true"} -SERVER_TYPE=${SERVER_TYPE:-"https"} -SKIP_HTTP_TOKEN_CHECK=${SKIP_HTTP_TOKEN_CHECK:-"false"} -SSLCONF=${SSLCONF:-"$(openssl version -d 2>/dev/null| cut -d\" -f2)/openssl.cnf"} -OCSP_MUST_STAPLE=${OCSP_MUST_STAPLE:-"false"} -TOKEN_USER_ID=${TOKEN_USER_ID:-""} -USE_SINGLE_ACL=${USE_SINGLE_ACL:-"false"} -VALIDATE_VIA_DNS=${VALIDATE_VIA_DNS:-""} -WORKING_DIR=${WORKING_DIR:-~/.getsslD} -_CHECK_ALL=${_CHECK_ALL:-"false"} -_CREATE_CONFIG=${_CREATE_CONFIG:-"false"} -_FORCE_RENEW=${_FORCE_RENEW:-"false"} -_QUIET=${_QUIET:-"false"} -_RECREATE_CSR=${_RECREATE_CSR:-"false"} -_REVOKE=${_REVOKE:-"false"} -_USE_DEBUG=${_USE_DEBUG:-"false"} +PREVIOUSLY_VALIDATED=${PREVIOUSLY_VALIDATED:="true"} +PRIVATE_KEY_ALG=${PRIVATE_KEY_ALG:="rsa"} +PUBLIC_DNS_SERVER=${PUBLIC_DNS_SERVER:="8.8.8.8"} +RELOAD_CMD=${RELOAD_CMD:=""} +RENEW_ALLOW=${RENEW_ALLOW:="30"} +REUSE_PRIVATE_KEY=${REUSE_PRIVATE_KEY:="true"} +SERVER_TYPE=${SERVER_TYPE:="https"} +SKIP_HTTP_TOKEN_CHECK=${SKIP_HTTP_TOKEN_CHECK:="false"} +SSLCONF=${SSLCONF:="$(openssl version -d 2>/dev/null| cut -d\" -f2)/openssl.cnf"} +OCSP_MUST_STAPLE=${OCSP_MUST_STAPLE:="false"} +TOKEN_USER_ID=${TOKEN_USER_ID:=""} +USE_SINGLE_ACL=${USE_SINGLE_ACL:="false"} +VALIDATE_VIA_DNS=${VALIDATE_VIA_DNS:=""} +WORKING_DIR=${WORKING_DIR:=~/.getsslD} +_CHECK_ALL=${_CHECK_ALL:="false"} +_CREATE_CONFIG=${_CREATE_CONFIG:="false"} +_FORCE_RENEW=${_FORCE_RENEW:="false"} +_QUIET=${_QUIET:="false"} +_RECREATE_CSR=${_RECREATE_CSR:="false"} +_REVOKE=${_REVOKE:="false"} +_USE_DEBUG=${_USE_DEBUG:="false"} config_errors="false" LANG=C From 69d5f164b23e058eafd816bf6b195091f2418623 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 10:49:29 -0800 Subject: [PATCH 27/58] Account generation function ported to `busybox`. Signed-off-by: Dan Schaper --- getsslD | 1744 +++---------------------------------------------------- 1 file changed, 87 insertions(+), 1657 deletions(-) diff --git a/getsslD b/getsslD index ab76be3..831fa0a 100755 --- a/getsslD +++ b/getsslD @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/ash # --------------------------------------------------------------------------- # getsslD - Obtain SSL certificates from the letsencrypt.org ACME server. # Runs in a Docker conatainer. @@ -16,391 +16,47 @@ # GNU General Public License at for # more details. -# For usage, run "getsslD -h" or see - PROGNAME=getsslD VERSION="1.0" # Default values, accepts environment variables if set, otherwise default are used +WORKING_DIR=${WORKING_DIR:="/ssl}" ACCOUNT_KEY_LENGTH=${ACCOUNT_KEY_LENGTH:="4096"} ACCOUNT_KEY_TYPE=${ACCOUNT_KEY_TYPE:="rsa"} -CA=${CA:="https://acme-staging.api.letsencrypt.org"} -CA_CERT_LOCATION=${CA_CERT_LOCATION:=""} -CHALLENGE_CHECK_TYPE=${CHALLENGE_CHECK_TYPE:="http"} -CHECK_ALL_AUTH_DNS=${CHECK_ALL_AUTH_DNS:="false"} -CHECK_REMOTE=${CHECK_REMOTE:="true"} -CHECK_REMOTE_WAIT=${CHECK_REMOTE_WAIT:="0"} -CSR_SUBJECT=${CSR_SUBJECT:="/"} -DEACTIVATE_AUTH=${DEACTIVATE_AUTH:="false"} -DEFAULT_REVOKE_CA=${DEFAULT_REVOKE_CA:="https://acme-v01.api.letsencrypt.org"} -DEFAULT_UMASK=${DEFAULT_UMASK:="u=rx,g=rx,o="} -DNS_EXTRA_WAIT=${DNS_EXTRA_WAIT:=""} -DNS_WAIT=${DNS_WAIT:="10"} -DOMAIN_KEY_LENGTH=${DOMAIN_KEY_LENGTH:="4096"} -DUAL_RSA_ECDSA=${DUAL_RSA_ECDSA:="false"} -GETSSLD_IGNORE_CP_PRESERVE=${GETSSLD_IGNORE_CP_PRESERVE:="false"} -HTTP_TOKEN_CHECK_WAIT=${HTTP_TOKEN_CHECK_WAIT:="0"} -IGNORE_DIRECTORY_DOMAIN=${IGNORE_DIRECTORY_DOMAIN:="false"} -ORIG_UMASK=$(umask) -PREVIOUSLY_VALIDATED=${PREVIOUSLY_VALIDATED:="true"} -PRIVATE_KEY_ALG=${PRIVATE_KEY_ALG:="rsa"} -PUBLIC_DNS_SERVER=${PUBLIC_DNS_SERVER:="8.8.8.8"} -RELOAD_CMD=${RELOAD_CMD:=""} -RENEW_ALLOW=${RENEW_ALLOW:="30"} -REUSE_PRIVATE_KEY=${REUSE_PRIVATE_KEY:="true"} -SERVER_TYPE=${SERVER_TYPE:="https"} -SKIP_HTTP_TOKEN_CHECK=${SKIP_HTTP_TOKEN_CHECK:="false"} -SSLCONF=${SSLCONF:="$(openssl version -d 2>/dev/null| cut -d\" -f2)/openssl.cnf"} -OCSP_MUST_STAPLE=${OCSP_MUST_STAPLE:="false"} -TOKEN_USER_ID=${TOKEN_USER_ID:=""} -USE_SINGLE_ACL=${USE_SINGLE_ACL:="false"} -VALIDATE_VIA_DNS=${VALIDATE_VIA_DNS:=""} -WORKING_DIR=${WORKING_DIR:=~/.getsslD} -_CHECK_ALL=${_CHECK_ALL:="false"} -_CREATE_CONFIG=${_CREATE_CONFIG:="false"} -_FORCE_RENEW=${_FORCE_RENEW:="false"} -_QUIET=${_QUIET:="false"} -_RECREATE_CSR=${_RECREATE_CSR:="false"} -_REVOKE=${_REVOKE:="false"} -_USE_DEBUG=${_USE_DEBUG:="false"} -config_errors="false" -LANG=C - -# Define all functions (in alphabetical order) - -cert_archive() { - # Archive certificates files - # Create directory for day, store certs by DOMAIN-YYYY_MM_DD:HH_MM UTC - - info "Copying generated certs to archive..." - - local date_time - date_time=$(date -u +%Y_%m_%d_%H_%M) - local date=${date_time::10} - local archive_dir="${DOMAIN_DIR}/archive/${date}" - local archive_suffix="${DOMAIN}-${date_time}" - - umask "${DEFAULT_UMASK}" - mkdir -p "${archive_dir}" - info " ${archive_dir} created." - - cp "${CERT_FILE}" "${archive_dir}/${archive_suffix}.crt" - cp "${DOMAIN_DIR}/${DOMAIN}.csr" "${archive_dir}/${archive_suffix}.csr" - cp "${DOMAIN_DIR}/${DOMAIN}.key" "${archive_dir}/${archive_suffix}.key" - cp "${CA_CERT}" "${archive_dir}/${archive_suffix}-chain.crt" - cat "$CERT_FILE" "$CA_CERT" > "${archive_dir}/${archive_suffix}-fullchain.crt" - info " RSA certs and chains copied." - - if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then - cp "${CERT_FILE::-4}.ec.crt" "${archive_dir}/${archive_suffix}.ec.crt" - cp "${DOMAIN_DIR}/${DOMAIN}.ec.csr" "${archive_dir}/${archive_suffix}.ec.csr" - cp "${DOMAIN_DIR}/${DOMAIN}.ec.key" "${archive_dir}/${archive_suffix}.ec.key" - cp "${CA_CERT::-4}.ec.crt" "${archive_dir}/${archive_suffix}-chain.ec.crt" - cat "${CERT_FILE::-4}.ec.crt" "${CA_CERT::-4}.ec.crt" > "${archive_dir}/${archive_suffix}-fullchain.ec.crt" - fi - info " EC certs and chains copied." - - umask "${ORIG_UMASK}" - - # Call purge_archive to clear out old files - info "Purging old getsslD archives" - purge_archive "${DOMAIN_DIR}" -} - -check_challenge_completion() { # checks with the ACME server if our challenge is OK - uri=$1 - g_domain=$2 - keyauthorization=$3 - - debug "sending request to ACME server saying we're ready for challenge" - send_signed_request "$uri" "{\"resource\": \"challenge\", \"keyAuthorization\": \"$keyauthorization\"}" +ACCOUNT_KEY=${ACCOUNT_KEY:="$WORKING_DIR/account.key"} - # check response from our request to perform challenge - if [[ ! -z "$code" ]] && [[ ! "$code" == '202' ]] ; then - error_exit "$g_domain:Challenge error: $code" - fi - - # loop "forever" to keep checking for a response from the ACME server. - while true ; do - debug "checking" - if ! get_cr "$uri" ; then - error_exit "$g_domain:Verify error:$code" - fi - - status=$(json_get "$response" status) - - # If ACME response is valid, then break out of loop - if [[ "$status" == "valid" ]] ; then - info "Verified $g_domain" - break; - fi - - # if ACME response is that their check gave an invalid response, error exit - if [[ "$status" == "invalid" ]] ; then - err_detail=$(json_get "$response" detail) - error_exit "$g_domain:Verify error:$err_detail" - fi - # if ACME response is pending ( they haven't completed checks yet) then wait and try again. - if [[ "$status" == "pending" ]] ; then - info "Pending" - else - error_exit "$g_domain:Verify error:$response" - fi - debug "sleep 5 secs before testing verify again" - sleep 5 - done - - if [[ "$DEACTIVATE_AUTH" == "true" ]]; then - deactivate_url=$(echo "$responseHeaders" | grep "^Link" | awk -F"[<>]" '{print $2}') - deactivate_url_list="$deactivate_url_list $deactivate_url" - debug "adding url to deactivate list - $deactivate_url" - fi -} +##### +# Functions +##### -check_config() { # check the config files for all obvious errors - debug "checking config" +create_account_key() { + # Create account key - # check keys - case "$ACCOUNT_KEY_TYPE" in - rsa|prime256v1|secp384r1|secp521r1) - debug "checked ACCOUNT_KEY_TYPE " ;; - *) - info "${DOMAIN}: invalid ACCOUNT_KEY_TYPE - $ACCOUNT_KEY_TYPE" - config_errors=true ;; - esac - if [[ "$ACCOUNT_KEY" == "$DOMAIN_DIR/${DOMAIN}.key" ]]; then - info "${DOMAIN}: ACCOUNT_KEY and domain key ( $DOMAIN_DIR/${DOMAIN}.key ) must be different" - config_errors=true + # Set values to args otherwise use environment variables + # https://stackoverflow.com/a/13864829 + if [[ ! -z ${1+x} ]]; then + ACCOUNT_KEY_LENGTH="${1}" fi - case "$PRIVATE_KEY_ALG" in - rsa|prime256v1|secp384r1|secp521r1) - debug "checked PRIVATE_KEY_ALG " ;; - *) - info "${DOMAIN}: invalid PRIVATE_KEY_ALG - $PRIVATE_KEY_ALG" - config_errors=true ;; - esac - if [[ "$DUAL_RSA_ECDSA" == "true" ]] && [[ "$PRIVATE_KEY_ALG" == "rsa" ]]; then - info "${DOMAIN}: PRIVATE_KEY_ALG not set to an EC type and DUAL_RSA_ECDSA=\"true\"" - config_errors=true + if [[ ! -z ${2+x} ]]; then + ACCOUNT_KEY="${2}" fi - - # get all domains - if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then - alldomains=${SANS//,/ } + if [[ -s ${ACCOUNT_KEY} ]]; then + printf '%s\n' "Account key exists at ${ACCOUNT_KEY} skipping generation." + return 0 + elif [[ ! -d $(dirname "${2}") ]]; then + print_error "Directory for storing ${2} does not exist." + return 1 else - alldomains=$(echo "$DOMAIN,$SANS" | sed "s/,/ /g") - fi - if [[ -z "$alldomains" ]]; then - info "${DOMAIN}: no domains specified" - config_errors=true - fi - - if [[ $VALIDATE_VIA_DNS == "true" ]]; then # using dns-01 challenge - if [[ -z "$DNS_ADD_COMMAND" ]]; then - info "${DOMAIN}: DNS_ADD_COMMAND not defined (whilst VALIDATE_VIA_DNS=\"true\")" - config_errors=true - fi - if [[ -z "$DNS_DEL_COMMAND" ]]; then - info "${DOMAIN}: DNS_DEL_COMMAND not defined (whilst VALIDATE_VIA_DNS=\"true\")" - config_errors=true - fi - fi - - dn=0 - tmplist=$(mktemp) - for d in $alldomains; do # loop over domains (dn is domain number) - debug "checking domain $d" - if [[ "$(grep "^${d}$" "$tmplist")" = "$d" ]]; then - info "${DOMAIN}: $d appears to be duplicated in domain, SAN list" - config_errors=true - else - echo "$d" >> "$tmplist" - fi - - if [[ "$USE_SINGLE_ACL" == "true" ]]; then - DOMAIN_ACL="${ACL[0]}" - else - DOMAIN_ACL="${ACL[$dn]}" - fi - - if [[ $VALIDATE_VIA_DNS != "true" ]]; then # using http-01 challenge - if [[ -z "${DOMAIN_ACL}" ]]; then - info "${DOMAIN}: ACL location not specified for domain $d in $DOMAIN_DIR/getsslD.cfg" - config_errors=true - fi - # check domain exist - if [[ "$(nslookup -query=AAAA "${d}"|grep -c "^${d}.*has AAAA address")" -ge 1 ]]; then - debug "found IPv6 record for ${d}" - elif [[ "$(nslookup "${d}"| grep -c ^Name)" -ge 1 ]]; then - debug "found IPv4 record for ${d}" - else - info "${DOMAIN}: DNS lookup failed for $d" - config_errors=true - fi - fi # end using http-01 challenge - ((dn++)) - done - - # tidy up - rm -f "$tmplist" - - if [[ "$config_errors" == "true" ]]; then - error_exit "${DOMAIN}: exiting due to config errors" - fi - debug "${DOMAIN}: check_config completed - all OK" -} - -clean_up() { # Perform pre-exit housekeeping - umask "$ORIG_UMASK" - if [[ $VALIDATE_VIA_DNS == "true" ]]; then - # Tidy up DNS entries if things failed part way though. - shopt -s nullglob - for dnsfile in $TEMP_DIR/dns_verify/*; do - # shellcheck source=/dev/null - . "$dnsfile" - debug "attempting to clean up DNS entry for $d" - eval "$DNS_DEL_COMMAND" "$d" "$auth_key" - done - shopt -u nullglob - fi - if [[ ! -z "$DOMAIN_DIR" ]]; then - rm -rf "${TEMP_DIR:?}" - fi -} - -copy_file_to_location() { # copies a file, using scp, sftp or ftp if required. - cert=$1 # descriptive name, just used for display - from=$2 # current file location - to=$3 # location to move file to. - IFS=\; read -r -a copy_locations <<<"$3" - for to in "${copy_locations[@]}"; do - info "copying $cert to $to" - debug "copying from $from to $to" - if [[ "${to:0:4}" == "ssh:" ]] ; then - debug "using scp scp -q $from ${to:4}" - if ! scp -q "$from" "${to:4}" >/dev/null 2>&1 ; then - error_exit "problem copying file to the server using scp. - scp $from ${to:4}" - fi - debug "userid $TOKEN_USER_ID" - if [[ "$cert" == "challenge token" ]] && [[ ! -z "$TOKEN_USER_ID" ]]; then - servername=$(echo "$to" | awk -F":" '{print $2}') - tofile=$(echo "$to" | awk -F":" '{print $3}') - debug "servername $servername" - debug "file $tofile" - # shellcheck disable=SC2029 - ssh "$servername" "chown $TOKEN_USER_ID $tofile" - fi - elif [[ "${to:0:4}" == "ftp:" ]] ; then - if [[ "$cert" != "challenge token" ]] ; then - error_exit "ftp is not a sercure method for copying certificates or keys" - fi - debug "using ftp to copy the file from $from" - ftpuser=$(echo "$to"| awk -F: '{print $2}') - ftppass=$(echo "$to"| awk -F: '{print $3}') - ftphost=$(echo "$to"| awk -F: '{print $4}') - ftplocn=$(echo "$to"| awk -F: '{print $5}') - ftpdirn=$(dirname "$ftplocn") - ftpfile=$(basename "$ftplocn") - fromdir=$(dirname "$from") - fromfile=$(basename "$from") - debug "ftp user=$ftpuser - pass=$ftppass - host=$ftphost dir=$ftpdirn file=$ftpfile" - debug "from dir=$fromdir file=$fromfile" - ftp -n <<- _EOF - open $ftphost - user $ftpuser $ftppass - cd $ftpdirn - lcd $fromdir - put $fromfile - _EOF - elif [[ "${to:0:5}" == "sftp:" ]] ; then - debug "using sftp to copy the file from $from" - ftpuser=$(echo "$to"| awk -F: '{print $2}') - ftppass=$(echo "$to"| awk -F: '{print $3}') - ftphost=$(echo "$to"| awk -F: '{print $4}') - ftplocn=$(echo "$to"| awk -F: '{print $5}') - ftpdirn=$(dirname "$ftplocn") - ftpfile=$(basename "$ftplocn") - fromdir=$(dirname "$from") - fromfile=$(basename "$from") - debug "sftp user=$ftpuser - pass=$ftppass - host=$ftphost dir=$ftpdirn file=$ftpfile" - debug "from dir=$fromdir file=$fromfile" - sshpass -p "$ftppass" sftp "$ftpuser@$ftphost" <<- _EOF - cd $ftpdirn - lcd $fromdir - put $fromfile - _EOF - else - if ! mkdir -p "$(dirname "$to")" ; then - error_exit "cannot create ACL directory $(basename "$to")" - fi - if [[ "$GETSSLD_IGNORE_CP_PRESERVE" == "true" ]]; then - if ! cp "$from" "$to" ; then - error_exit "cannot copy $from to $to" - fi - else - if ! cp -p "$from" "$to" ; then - error_exit "cannot copy $from to $to" - fi - fi - if [[ "$cert" == "challenge token" ]] && [[ ! -z "$TOKEN_USER_ID" ]]; then - chown "$TOKEN_USER_ID" "$to" - fi - fi - debug "copied $from to $to" - done -} - -create_csr() { # create a csr using a given key (if it doesn't already exist) - csr_file=$1 - csr_key=$2 - # check if domain csr exists - if not then create it - if [[ -s "$csr_file" ]]; then - debug "domain csr exists at - $csr_file" - # check all domains in config are in csr - if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then - alldomains=$(echo "$SANS" | sed -e 's/ //g; s/,$//; y/,/\n/' | sort -u) - else - alldomains=$(echo "$DOMAIN,$SANS" | sed -e 's/ //g; s/,$//; y/,/\n/' | sort -u) - fi - domains_in_csr=$(openssl req -text -noout -in "$csr_file" \ - | sed -n -e 's/^ *Subject: .* CN=\([A-Za-z0-9.-]*\).*$/\1/p; /^ *DNS:.../ { s/ *DNS://g; y/,/\n/; p; }' \ - | sort -u) - for d in $alldomains; do - if [[ "$(echo "${domains_in_csr}"| grep "^${d}$")" != "${d}" ]]; then - info "existing csr at $csr_file does not contain ${d} - re-create-csr"\ - ".... $(echo "${domains_in_csr}"| grep "^${d}$")" - _RECREATE_CSR="true" - fi - done - # check all domains in csr are in config - if [[ "$alldomains" != "$domains_in_csr" ]]; then - info "existing csr at $csr_file does not have the same domains as the config - re-create-csr" - _RECREATE_CSR="true" - fi - fi - # end of ... check if domain csr exists - if not then create it - - # if CSR does not exist, or flag set to recreate, then create csr - if [[ ! -s "$csr_file" ]] || [[ "$_RECREATE_CSR" == "true" ]]; then - info "creating domain csr - $csr_file" - # create a temporary config file, for portability. - tmp_conf=$(mktemp) - cat "$SSLCONF" > "$tmp_conf" - printf "[SAN]\n%s" "$SANLIST" >> "$tmp_conf" - # add OCSP Must-Staple to the domain csr - # if openssl version >= 1.1.0 one can also use "tlsfeature = status_request" - if [[ "$OCSP_MUST_STAPLE" == "true" ]]; then - printf "\n1.3.6.1.5.5.7.1.24 = DER:30:03:02:01:05" >> "$tmp_conf" - fi - openssl req -new -sha256 -key "$csr_key" -subj "$CSR_SUBJECT" -reqexts SAN -config "$tmp_conf" > "$csr_file" - rm -f "$tmp_conf" + printf '%s\n' "Creating account key ${ACCOUNT_KEY}:" + create_key "${ACCOUNT_KEY}" "${ACCOUNT_KEY_LENGTH}" fi + return 0 } create_key() { # Create an openSSL key + local key_loc=${1} local key_len=${2} local key_type @@ -418,663 +74,108 @@ create_key() { elif [[ "${key_len}" -eq 521 ]]; then key_type="secp521r1" else - error "Invalid key length. Please check you configuration." + print_error "Invalid key length. Please check your configuration." return 1 fi case "$key_type" in RSA) + printf '\t%s' "Creating ${key_len} bit RSA key..." openssl genrsa -out "${key_loc}" "${key_len}" >& /dev/null + printf '%s\n' "Done." return 0 ;; prime256v1|secp384r1|secp521r1) + printf '\t%s' "Creating ${key_len} bit ECC key..." openssl ecparam -genkey -out "${key_loc}" -name "${key_type}" >& /dev/null + printf '%s\n' "Done." return 0 ;; esac # Error inside case statement openssl generation + print_error "Error creating OpenSSL key, deleting key..." rm "${key_loc}" + print_error "Done.\n" return 1 } -date_epoc() { # convert the date into epoch time - date -D "%b %d %T %Y" -d "$(echo "$1" | awk '{print $1 $2 $3 $4}')" +%s +get_date() { + # get current date and time in UTC YYYY-MM-DDTHH:MM:SSZ + echo $(date -u +"%Y-%m-%dT%H:%M:%SZ") } -date_fmt() { # format date from epoc time to YYYY-MM-DD - date -d "@$1" +%F -} - -date_renew() { # calculates the renewal time in epoch - date_now_s=$( date +%s ) - echo "$((date_now_s + RENEW_ALLOW*24*60*60))" -} - -debug() { # write out debug info if the debug flag has been set - if [[ ${_USE_DEBUG} == "true" ]]; then - echo " " - echo "$@" - fi -} +help_message() { + # Print help message -error_exit() { # give error message on error exit - echo -e "${PROGNAME}: ${1:-"Unknown Error"}" >&2 - clean_up - exit 1 -} - -error() { - # Write error message to STDERR for log. - echo "$@" >&2 -} - -get_auth_dns() { - # Find authoritative DNS server for domain via SOA lookup. - local g_domain="$1" - local g_server="$PUBLIC_DNS_SERVER" - local result - - result=$(nslookup -debug=1 -type=soa -type=ns "${g_domain}" "${g_server}") - - if echo "${result}" | grep -q "Non-authoritative"; then - # this is a Non-authoritative server, need to check for an authoritative one. - gad_s=$(echo "${result}" | awk '$2 ~ "nameserver" {print $4; exit }' |sed 's/\.$//g') - if [[ "$(echo "${result}" | grep -c "an't find")" -gt 0 ]]; then - # if domain name doesn't exist, then find auth servers for next level up - gad_s=$(echo "${result}" | awk '$1 ~ "origin" {print $3; exit }') - g_domain=$(echo "${result}" | awk '$1 ~ "->" {print $2; exit}') - fi - fi - - if [[ -z "$gad_s" ]]; then - res=$(nslookup -debug=1 -type=soa -type=ns "${g_domain}") - else - res=$(nslookup -debug=1 -type=soa -type=ns "${g_domain}" "${g_server}") - fi - - if [[ "$(echo "$res" | grep -c "canonical name")" -gt 0 ]]; then - g_domain=$(echo "$res" | awk ' $2 ~ "canonical" {print $5; exit }' |sed 's/\.$//g') - elif [[ "$(echo "$res" | grep -c "an't find")" -gt 0 ]]; then - gad_s=$(echo "$res" | awk ' $1 ~ "origin" {print $3; exit }') - g_domain=$(echo "$res"| awk '$1 ~ "->" {print $2; exit}') - fi - - all_auth_dns_servers=$(nslookup -type=soa -type=ns "${g_domain}" "${g_server}" \ - | awk ' $2 ~ "nameserver" {print $4}' \ - | sed 's/\.$//g'| tr '\n' ' ') - if [[ $CHECK_ALL_AUTH_DNS == "true" ]]; then - primary_ns="$all_auth_dns_servers" - else - primary_ns=$(echo "$all_auth_dns_servers" | awk '{print $1}') - fi -} - -get_certificate() { # get certificate for csr, if all domains validated. - gc_csr=$1 # the csr file - gc_certfile=$2 # The filename for the certificate - gc_cafile=$3 # The filename for the CA certificate - - der=$(openssl req -in "$gc_csr" -outform DER | urlbase64) - debug "der $der" - send_signed_request "$URL_new_cert" "{\"resource\": \"new-cert\", \"csr\": \"$der\"}" "needbase64" - - # convert certificate information into correct format and save to file. - CertData=$(awk ' $1 ~ "^Location" {print $2}' "$CURL_HEADER" |tr -d '\r') - debug "certdata location = $CertData" - if [[ "$CertData" ]] ; then - echo -----BEGIN CERTIFICATE----- > "$gc_certfile" - curl --silent "$CertData" | openssl base64 -e >> "$gc_certfile" - echo -----END CERTIFICATE----- >> "$gc_certfile" - info "Certificate saved in $CERT_FILE" - fi - - # If certificate wasn't a valid certificate, error exit. - if [[ -z "$CertData" ]] ; then - response2=$(echo "$response" | fold -w64 |openssl base64 -d) - debug "response was $response" - error_exit "Sign failed: $(echo "$response2" | grep "detail")" - fi - - # get a copy of the CA certificate. - IssuerData=$(grep -i '^Link' "$CURL_HEADER" \ - | cut -d " " -f 2\ - | cut -d ';' -f 1 \ - | sed 's///g') - if [[ "$IssuerData" ]] ; then - echo -----BEGIN CERTIFICATE----- > "$gc_cafile" - curl --silent "$IssuerData" | openssl base64 -e >> "$gc_cafile" - echo -----END CERTIFICATE----- >> "$gc_cafile" - info "The intermediate CA cert is in $gc_cafile" - fi -} - -get_cr() { # get curl response - url="$1" - debug url "$url" - response=$(curl --silent "$url") - ret=$? - debug response "$response" - code=$(json_get "$response" status) - debug code "$code" - debug "get_cr return code $ret" - return $ret -} - -get_signing_params() { # get signing parameters from key - skey=$1 - if openssl rsa -in "${skey}" -noout 2>/dev/null ; then # RSA key - pub_exp64=$(openssl rsa -in "${skey}" -noout -text \ - | grep publicExponent \ - | grep -oE "0x[a-f0-9]+" \ - | cut -d'x' -f2 \ - | hex2bin \ - | urlbase64) - pub_mod64=$(openssl rsa -in "${skey}" -noout -modulus \ - | cut -d'=' -f2 \ - | hex2bin \ - | urlbase64) - - jwk='{"e":"'"${pub_exp64}"'","kty":"RSA","n":"'"${pub_mod64}"'"}' - jwkalg="RS256" - signalg="sha256" - elif openssl ec -in "${skey}" -noout 2>/dev/null ; then # Elliptic curve key. - crv="$(openssl ec -in "$skey" -noout -text 2>/dev/null | awk '$2 ~ "CURVE:" {print $3}')" - if [[ -z "$crv" ]]; then - gsp_keytype="$(openssl ec -in "$skey" -noout -text 2>/dev/null \ - | grep "^ASN1 OID:" \ - | awk '{print $3}')" - case "$gsp_keytype" in - prime256v1) crv="P-256" ;; - secp384r1) crv="P-384" ;; - secp521r1) crv="P-521" ;; - *) error_exit "invalid curve algorithm type $gsp_keytype";; - esac - fi - case "$crv" in - P-256) jwkalg="ES256" ; signalg="sha256" ;; - P-384) jwkalg="ES384" ; signalg="sha384" ;; - P-521) jwkalg="ES512" ; signalg="sha512" ;; - *) error_exit "invalid curve algorithm type $crv";; - esac - pubtext="$(openssl ec -in "$skey" -noout -text 2>/dev/null \ - | awk '/^pub:/{p=1;next}/^ASN1 OID:/{p=0}p' \ - | tr -d ": \n\r")" - mid=$(( (${#pubtext} -2) / 2 + 2 )) - debug "pubtext = $pubtext" - x64=$(echo "$pubtext" | cut -b 3-$mid | hex2bin | urlbase64) - y64=$(echo "$pubtext" | cut -b $((mid+1))-${#pubtext} | hex2bin | urlbase64) - jwk='{"crv":"'"$crv"'","kty":"EC","x":"'"$x64"'","y":"'"$y64"'"}' - debug "jwk $jwk" - else - error_exit "Invalid key file" - fi - thumbprint="$(printf "%s" "$jwk" | openssl dgst -sha256 -binary | urlbase64)" - debug "jwk alg = $jwkalg" - debug "jwk = $jwk" - debug "thumbprint $thumbprint" -} - -graceful_exit() { # normal exit function. - clean_up - exit -} - -help_message() { # print out the help message cat <<- _EOF_ - Obtain SSL certificates from the letsencrypt.org ACME server - - $(usage) - - Options: - -a, --all Check all certificates - -d, --debug Output debug information - -c, --create DOMAIN Create default configuration files - -f, --force Force renewal of cert - override expiry checks - -h, --help Display this help message and exit - -q, --quiet Quiet mode - only outputs on error or success of new cert - -r, --revoke CERT KEY [CA SERVER] Revoke a certificate - + Usage: "${PROGNAME}" [OPTION]... [ARGS]... + Obtain SSL certificates from the letsencrypt.org ACME server. + + Options to long options apply to short options also. + Options: + -a, --account [LENGTH] [FILE] Create an account key of LENGTH with FILE name. + LENGTH is 2048-4096 for RSA keys, 256|384|521 for ECC keys. + Defaults to 4096 bit RSA key in account.key. _EOF_ + return 0 } -hex2bin() { # Remove spaces, add leading zero, escape as hex string ensuring no trailing new line char - echo -e -n "$(cat | sed -r -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')" -} - -info() { # write out info as long as the quiet flag has not been set. - if [[ ${_QUIET} != "true" ]]; then - echo "$@" - fi -} - -json_get() { # get the value corresponding to $2 in the JSON passed as $1. - # remove newlines, so it's a single chunk of JSON - json_data=$( echo "$1" | tr '\n' ' ') - # if $3 is defined, this is the section which the item is in. - if [[ ! -z "$3" ]]; then - jg_section=$(echo "$json_data" | awk -F"[}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${3}"'\"/){print $i}}}') - if [[ "$2" == "uri" ]]; then - jg_subsect=$(echo "$jg_section" | awk -F"[,]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i)}}}') - jg_result=$(echo "$jg_subsect" | awk -F'"' '{print $4}') - else - jg_result=$(echo "$jg_section" | awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i+1)}}}') - fi - else - jg_result=$(echo "$json_data" |awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i+1)}}}') - fi - # check number of quotes - jg_q=${jg_result//[^\"]/} - # if 2 quotes, assume it's a quoted variable and just return the data within the quotes. - if [[ ${#jg_q} -eq 2 ]]; then - echo "$jg_result" | awk -F'"' '{print $2}' - else - echo "$jg_result" - fi -} - -purge_archive() { # purge archive of old, invalid, certificates - arcdir="$1/archive" - debug "purging archives in ${arcdir}/" - for padir in $arcdir/????_??_??_??_??; do - # check each directory - if [[ -d "$padir" ]]; then - tstamp=$(basename "$padir"| awk -F"_" '{print $1"-"$2"-"$3" "$4":"$5}') - direpoc=$(date -d "$tstamp" +%s) - current_epoc=$(date "+%s") - # as certs currently valid for 90 days, purge anything older than 100 - purgedate=$((current_epoc - 60*60*24*100)) - if [[ "$direpoc" -lt "$purgedate" ]]; then - echo "purge $padir" - rm -rf "${padir:?}" - fi - fi - done -} +prep_workdir() { + # Prepare working directory for key/cert functions -reload_service() { # Runs a command to reload services ( via ssh if needed) - if [[ ! -z "$RELOAD_CMD" ]]; then - info "reloading SSL services" - if [[ "${RELOAD_CMD:0:4}" == "ssh:" ]] ; then - sshhost=$(echo "$RELOAD_CMD"| awk -F: '{print $2}') - command=${RELOAD_CMD:(( ${#sshhost} + 5))} - debug "running following command to reload cert" - debug "ssh $sshhost ${command}" - # shellcheck disable=SC2029 - ssh "$sshhost" "${command}" 1>/dev/null 2>&1 - # allow 2 seconds for services to restart - sleep 2 + if [[ ! -d "${WORKING_DIR}" ]]; then + printf '%s' "Creating getsslD certificate storage directory - ${WORKING_DIR}..." + if ! mkdir -p "${WORKING_DIR}" >& /dev/null; then + print_error "Could not create ${WORKING_DIR}. Check volumes.\n" + exit 1 else - debug "running reload command $RELOAD_CMD" - if ! eval "$RELOAD_CMD" ; then - error_exit "error running $RELOAD_CMD" - fi + printf '%s\n' "Done." fi fi + return 0 } -revoke_certificate() { # revoke a certificate - debug "revoking cert $REVOKE_CERT" - debug "using key $REVOKE_KEY" - ACCOUNT_KEY="$REVOKE_KEY" - # need to set the revoke key as "account_key" since it's used in send_signed_request. - get_signing_params "$REVOKE_KEY" - TEMP_DIR=$(mktemp -d) - debug "revoking from $CA" - rcertdata=$(openssl x509 -in "$REVOKE_CERT" -inform PEM -outform DER | urlbase64) - send_signed_request "$URL_revoke" "{\"resource\": \"revoke-cert\", \"certificate\": \"$rcertdata\"}" - if [[ $code -eq "200" ]]; then - info "certificate revoked" - else - error_exit "Revocation failed: $(echo "$response" | grep "detail")" - fi -} +print_error() { + # Output error messages to STDERR + local error=$1 -set_server_type() { # uses SERVER_TYPE to set REMOTE_PORT and REMOTE_EXTRA - if [[ ${SERVER_TYPE} == "https" ]] || [[ ${SERVER_TYPE} == "webserver" ]]; then - REMOTE_PORT=443 - elif [[ ${SERVER_TYPE} == "ftp" ]]; then - REMOTE_PORT=21 - REMOTE_EXTRA="-starttls ftp" - elif [[ ${SERVER_TYPE} == "ftpi" ]]; then - REMOTE_PORT=990 - elif [[ ${SERVER_TYPE} == "imap" ]]; then - REMOTE_PORT=143 - REMOTE_EXTRA="-starttls imap" - elif [[ ${SERVER_TYPE} == "imaps" ]]; then - REMOTE_PORT=993 - elif [[ ${SERVER_TYPE} == "pop3" ]]; then - REMOTE_PORT=110 - REMOTE_EXTRA="-starttls pop3" - elif [[ ${SERVER_TYPE} == "pop3s" ]]; then - REMOTE_PORT=995 - elif [[ ${SERVER_TYPE} == "smtp" ]]; then - REMOTE_PORT=25 - REMOTE_EXTRA="-starttls smtp" - elif [[ ${SERVER_TYPE} == "smtps_deprecated" ]]; then - REMOTE_PORT=465 - elif [[ ${SERVER_TYPE} == "smtps" ]] || [[ ${SERVER_TYPE} == "smtp_submission" ]]; then - REMOTE_PORT=587 - REMOTE_EXTRA="-starttls smtp" - elif [[ ${SERVER_TYPE} == "xmpp" ]]; then - REMOTE_PORT=5222 - REMOTE_EXTRA="-starttls xmpp" - elif [[ ${SERVER_TYPE} == "xmpps" ]]; then - REMOTE_PORT=5269 - elif [[ ${SERVER_TYPE} == "ldaps" ]]; then - REMOTE_PORT=636 - elif [[ ${SERVER_TYPE} =~ ^[0-9]+$ ]]; then - REMOTE_PORT=${SERVER_TYPE} - else - info "${DOMAIN}: unknown server type \"$SERVER_TYPE\" in SERVER_TYPE" - config_errors=true - fi + printf '!! %s\n' "${1}" 1>&2 + return 0 } -send_signed_request() { # Sends a request to the ACME server, signed with your private key. - url=$1 - payload=$2 - needbase64=$3 - - debug url "$url" - debug payload "$payload" - - CURL_HEADER="$TEMP_DIR/curl.header" - dp="$TEMP_DIR/curl.dump" - CURL="curl --silent --dump-header $CURL_HEADER " - if [[ ${_USE_DEBUG} == "true" ]]; then - CURL="$CURL --trace-ascii $dp " - fi - - # convert payload to url base 64 - payload64="$(printf '%s' "${payload}" | urlbase64)" - debug payload64 "$payload64" - - # get nonce from ACME server - nonceurl="$CA/directory" - nonce=$($CURL -I $nonceurl | grep "^Replay-Nonce:" | awk '{print $2}' | tr -d '\r\n ') - - debug nonce "$nonce" - - # Build header with just our public key and algorithm information - header='{"alg": "'"$jwkalg"'", "jwk": '"$jwk"'}' - - # Build another header which also contains the previously received nonce and encode it as urlbase64 - protected='{"alg": "'"$jwkalg"'", "jwk": '"$jwk"', "nonce": "'"${nonce}"'", "url": "'"${url}"'"}' - protected64="$(printf '%s' "${protected}" | urlbase64)" - debug protected "$protected" - - # Sign header with nonce and our payload with our private key and encode signature as urlbase64 - sign_string "$(printf '%s' "${protected64}.${payload64}")" "${ACCOUNT_KEY}" "$signalg" - - # Send header + extended header + payload + signature to the acme-server - body="{\"header\": ${header}," - body="${body}\"protected\": \"${protected64}\"," - body="${body}\"payload\": \"${payload64}\"," - body="${body}\"signature\": \"${signed64}\"}" - debug "header, payload and signature = $body" - - code="500" - loop_limit=5 - while [[ "$code" -eq 500 ]]; do - if [[ "$needbase64" ]] ; then - response=$($CURL -X POST --data "$body" "$url" | urlbase64) - else - response=$($CURL -X POST --data "$body" "$url") - fi - - responseHeaders=$(cat "$CURL_HEADER") - debug responseHeaders "$responseHeaders" - debug response "$response" - code=$(awk ' $1 ~ "^HTTP" {print $2}' "$CURL_HEADER" | tail -1) - debug code "$code" - response_status=$(json_get "$response" status \ - | head -1| awk -F'"' '{print $2}') - debug "response status = $response_status" +arg_parser() { + # Check CLI arguments and process - if [[ "$code" -eq 500 ]]; then - info "error on acme server - trying again ...." - sleep 2 - loop_limit=$((loop_limit - 1)) - if [[ $loop_limit -lt 1 ]]; then - error_exit "500 error from ACME server: $response" - fi + while [[ -n ${1} ]]; do + case $1 in + -a | --account) + shift + create_account_key $* + exit 0 + ;; + -h | --help) + help_message; + exit 0 + ;; + *) + printf '%s\n\n' "Invalid option." + help_message + exit 1 + ;; + esac + shift + if [[ -z ${1} ]]; then + break fi done } -sign_string() { # sign a string with a given key and algorithm and return urlbase64 - # sets the result in variable signed64 - str=$1 - key=$2 - signalg=$3 - - if openssl rsa -in "${skey}" -noout 2>/dev/null ; then # RSA key - signed64="$(printf '%s' "${str}" | openssl dgst -"$signalg" -sign "$key" | urlbase64)" - elif openssl ec -in "${skey}" -noout 2>/dev/null ; then # Elliptic curve key. - signed=$(printf '%s' "${str}" | openssl dgst -"$signalg" -sign "$key" -hex | awk '{print $2}') - debug "EC signature $signed" - if [[ "${signed:4:4}" == "0220" ]]; then #sha256 - R=$(echo "$signed" | cut -c 9-72) - part2=$(echo "$signed" | cut -c 73-) - elif [[ "${signed:4:4}" == "0221" ]]; then #sha256 - R=$(echo "$signed" | cut -c 11-74) - part2=$(echo "$signed" | cut -c 75-) - elif [[ "${signed:4:4}" == "0230" ]]; then #sha384 - R=$(echo "$signed" | cut -c 9-104) - part2=$(echo "$signed" | cut -c 105-) - elif [[ "${signed:4:4}" == "0231" ]]; then #sha384 - R=$(echo "$signed" | cut -c 11-106) - part2=$(echo "$signed" | cut -c 107-) - elif [[ "${signed:6:4}" == "0241" ]]; then #sha512 - R=$(echo "$signed" | cut -c 11-140) - part2=$(echo "$signed" | cut -c 141-) - elif [[ "${signed:6:4}" == "0242" ]]; then #sha512 - R=$(echo "$signed" | cut -c 11-142) - part2=$(echo "$signed" | cut -c 143-) - else - error_exit "error in EC signing couldn't get R from $signed" - fi - debug "R $R" - - if [[ "${part2:0:4}" == "0220" ]]; then #sha256 - S=$(echo "$part2" | cut -c 5-68) - elif [[ "${part2:0:4}" == "0221" ]]; then #sha256 - S=$(echo "$part2" | cut -c 7-70) - elif [[ "${part2:0:4}" == "0230" ]]; then #sha384 - S=$(echo "$part2" | cut -c 5-100) - elif [[ "${part2:0:4}" == "0231" ]]; then #sha384 - S=$(echo "$part2" | cut -c 7-102) - elif [[ "${part2:0:4}" == "0241" ]]; then #sha512 - S=$(echo "$part2" | cut -c 5-136) - elif [[ "${part2:0:4}" == "0242" ]]; then #sha512 - S=$(echo "$part2" | cut -c 5-136) - else - error_exit "error in EC signing couldn't get S from $signed" - fi - - debug "S $S" - signed64=$(printf '%s' "${R}${S}" | hex2bin | urlbase64 ) - debug "encoded RS $signed64" - fi -} - -signal_exit() { # Handle trapped signals - case $1 in - INT) - error_exit "Program interrupted by user" ;; - TERM) - echo -e "\n$PROGNAME: Program terminated" >&2 - graceful_exit ;; - *) - error_exit "$PROGNAME: Terminating on unknown signal" ;; - esac -} - -urlbase64() { # urlbase64: base64 encoded string with '+' replaced with '-' and '/' replaced with '_' - openssl base64 -e | tr -d '\n\r' | sed -r -e 's:=*$::g' -e 'y:+/:-_:' -} - -usage() { # echos out the program usage - echo "Usage: $PROGNAME [options] [ARGS]" -} - -write_domain_template() { # write out a template file for a domain. - cat > "$1" <<- _EOF_domain_ - # Uncomment and modify any variables you need - # see https://github.com/dschaper/getsslD/wiki/Config-variables for details - # see https://github.com/dschaper/getsslD/wiki/Example-config-files for example configs - # - # The staging server is best for testing - #CA="https://acme-staging.api.letsencrypt.org" - # This server issues full certificates, however has rate limits - #CA="https://acme-v01.api.letsencrypt.org" - - #PRIVATE_KEY_ALG="rsa" - - # Additional domains - this could be multiple domains / subdomains in a comma separated list - # Note: this is Additional domains - so should not include the primary domain. - SANS="${EX_SANS}" - - # Acme Challenge Location. The first line for the domain, the following ones for each additional domain. - # If these start with ssh: then the next variable is assumed to be the hostname and the rest the location. - # An ssh key will be needed to provide you with access to the remote server. - # Optionally, you can specify a different userid for ssh/scp to use on the remote server before the @ sign. - # If left blank, the username on the local server will be used to authenticate against the remote server. - # If these start with ftp: then the next variables are ftpuserid:ftppassword:servername:ACL_location - # These should be of the form "/path/to/your/website/folder/.well-known/acme-challenge" - # where "/path/to/your/website/folder/" is the path, on your web server, to the web root for your domain. - #ACL=('/var/www/${DOMAIN}/web/.well-known/acme-challenge' - # 'ssh:server5:/var/www/${DOMAIN}/web/.well-known/acme-challenge' - # 'ssh:sshuserid@server5:/var/www/${DOMAIN}/web/.well-known/acme-challenge' - # 'ftp:ftpuserid:ftppassword:${DOMAIN}:/web/.well-known/acme-challenge') - - #Set USE_SINGLE_ACL="true" to use a single ACL for all checks - #USE_SINGLE_ACL="false" - - # Location for all your certs, these can either be on the server (full path name) - # or using ssh /sftp as for the ACL - #DOMAIN_CERT_LOCATION="/etc/ssl/${DOMAIN}.crt" - #DOMAIN_KEY_LOCATION="/etc/ssl/${DOMAIN}.key" - #CA_CERT_LOCATION="/etc/ssl/chain.crt" - #DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert - #DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert - - # The command needed to reload apache / nginx or whatever you use - #RELOAD_CMD="" - - # Define the server type. This can be https, ftp, ftpi, imap, imaps, pop3, pop3s, smtp, - # smtps_deprecated, smtps, smtp_submission, xmpp, xmpps, ldaps or a port number which - # will be checked for certificate expiry and also will be checked after - # an update to confirm correct certificate is running (if CHECK_REMOTE) is set to true - #SERVER_TYPE="https" - #CHECK_REMOTE="true" - _EOF_domain_ -} - -write_getsslD_template() { # write out the main template file - cat > "$1" <<- _EOF_getsslD_ - # Uncomment and modify any variables you need - # see https://github.com/dschaper/getsslD/wiki/Config-variables for details - # - # The staging server is best for testing (hence set as default) - CA="https://acme-staging.api.letsencrypt.org" - # This server issues full certificates, however has rate limits - #CA="https://acme-v01.api.letsencrypt.org" - - #AGREEMENT="$AGREEMENT" - - # Set an email address associated with your account - generally set at account level rather than domain. - #ACCOUNT_EMAIL="me@example.com" - ACCOUNT_KEY_LENGTH=4096 - ACCOUNT_KEY="$WORKING_DIR/account_key.pem" - PRIVATE_KEY_ALG="rsa" - #REUSE_PRIVATE_KEY="true" - - # The command needed to reload apache / nginx or whatever you use - #RELOAD_CMD="" - # The time period within which you want to allow renewal of a certificate - # this prevents hitting some of the rate limits. - RENEW_ALLOW="30" - - # Define the server type. This can be https, ftp, ftpi, imap, imaps, pop3, pop3s, smtp, - # smtps_deprecated, smtps, smtp_submission, xmpp, xmpps, ldaps or a port number which - # will be checked for certificate expiry and also will be checked after - # an update to confirm correct certificate is running (if CHECK_REMOTE) is set to true - SERVER_TYPE="https" - CHECK_REMOTE="true" - - # Use the following 3 variables if you want to validate via DNS - #VALIDATE_VIA_DNS="true" - #DNS_ADD_COMMAND= - #DNS_DEL_COMMAND= - _EOF_getsslD_ -} - -write_openssl_conf() { # write out a minimal openssl conf - cat > "$1" <<- _EOF_openssl_conf_ - # minimal openssl.cnf file - distinguished_name = req_distinguished_name - [ req_distinguished_name ] - [v3_req] - [v3_ca] - _EOF_openssl_conf_ -} - -# Trap signals -trap "signal_exit TERM" TERM HUP -trap "signal_exit INT" INT - -# Parse command-line -while [[ -n ${1+defined} ]]; do - case $1 in - -h | --help) - help_message; graceful_exit ;; - -d | --debug) - _USE_DEBUG="true" ;; - -c | --create) - _CREATE_CONFIG="true" ;; - -f | --force) - _FORCE_RENEW="true" ;; - -a | --all) - _CHECK_ALL="true" ;; - -q | --quiet) - _QUIET="true" ;; - -r | --revoke) - _REVOKE="true" - shift - REVOKE_CERT="$1" - shift - REVOKE_KEY="$1" - shift - REVOKE_CA="$1" ;; - -w) - shift; WORKING_DIR="$1" ;; - -* | --*) - usage - error_exit "Unknown option $1" ;; - *) - if [[ ! -z $DOMAIN ]]; then - error_exit "invalid command line $DOMAIN - it appears to contain more than one domain" - fi - DOMAIN="$1" - if [[ -z $DOMAIN ]]; then - error_exit "invalid command line - it appears to contain a null variable" - fi ;; - esac - shift -done - ##### # Main logic ##### -# if the "working directory" doesn't exist, then create it. -if [[ ! -d "$WORKING_DIR" ]]; then - debug "Making working directory - $WORKING_DIR" - mkdir -p "$WORKING_DIR" -fi +main() { # read any variables from config in working directory if [[ -s "$WORKING_DIR/getsslD.cfg" ]]; then @@ -1083,682 +184,11 @@ if [[ -s "$WORKING_DIR/getsslD.cfg" ]]; then . "$WORKING_DIR/getsslD.cfg" fi -# Define defaults for variables not set in the main config. -ACCOUNT_KEY="${ACCOUNT_KEY:=$WORKING_DIR/account_key.pem}" -DOMAIN_STORAGE="${DOMAIN_STORAGE:=$WORKING_DIR}" -DOMAIN_DIR="$DOMAIN_STORAGE/$DOMAIN" -CERT_FILE="$DOMAIN_DIR/${DOMAIN}.crt" -CA_CERT="$DOMAIN_DIR/chain.crt" -TEMP_DIR="$DOMAIN_DIR/tmp" - - -# create account key if it doesn't exist. -if [[ -s "$ACCOUNT_KEY" ]]; then - info "Account key exists at $ACCOUNT_KEY skipping generation" -else - info "Creating account key $ACCOUNT_KEY" - create_key "$ACCOUNT_KEY" "$ACCOUNT_KEY_LENGTH" -fi - - -# Revoke a certificate if requested -if [[ $_REVOKE == "true" ]]; then - if [[ -z $REVOKE_CA ]]; then - CA=$DEFAULT_REVOKE_CA - elif [[ "$REVOKE_CA" == "-d" ]]; then - _USE_DEBUG="true" - CA=$DEFAULT_REVOKE_CA - else - CA=$REVOKE_CA - fi - URL_revoke=$(curl "${CA}/directory" 2>/dev/null | grep "revoke-cert" | awk -F'"' '{print $4}') - revoke_certificate - graceful_exit -fi - -# get latest agreement from CA (as default) -AGREEMENT=$(curl -I "${CA}/terms" 2>/dev/null | awk '$1 ~ "Location:" {print $2}'|tr -d '\r') - -# if nothing in command line, print help and exit. -if [[ -z "$DOMAIN" ]] && [[ ${_CHECK_ALL} != "true" ]]; then - error "Domain is required for this option." - help_message - graceful_exit -fi - -# Set the OPENSSL_CONF environment variable so openssl knows which config to use -export OPENSSL_CONF=$SSLCONF - -# if "-a" option then check other parameters and create run for each domain. -if [[ ${_CHECK_ALL} == "true" ]]; then - info "Check all certificates" - - if [[ ${_CREATE_CONFIG} == "true" ]]; then - error_exit "cannot combine -c|--create with -a|--all" - fi - - if [[ ${_FORCE_RENEW} == "true" ]]; then - error_exit "cannot combine -f|--force with -a|--all because of rate limits" - fi - - if [[ ! -d "$DOMAIN_STORAGE" ]]; then - error_exit "DOMAIN_STORAGE not found - $DOMAIN_STORAGE" - fi - - for dir in ${DOMAIN_STORAGE}/*; do - if [[ -d "$dir" ]]; then - debug "Checking $dir" - cmd="$0" - if [[ ${_USE_DEBUG} == "true" ]]; then - cmd="$cmd -d" - fi - if [[ ${_QUIET} == "true" ]]; then - cmd="$cmd -q" - fi - # check if $dir looks like a domain name (contains a period) - if [[ $(basename "$dir") == *.* ]]; then - cmd="$cmd -w $WORKING_DIR $(basename "$dir")" - debug "CMD: $cmd" - eval "$cmd" - fi - fi - done - - graceful_exit -fi -# end of "-a" option (looping through all domains) - -# if "-c|--create" option used, then create config files. -if [[ ${_CREATE_CONFIG} == "true" ]]; then - # If main config file does not exists then create it. - if [[ ! -s "$WORKING_DIR/getsslD.cfg" ]]; then - info "creating main config file $WORKING_DIR/getsslD.cfg" - if [[ ! -s "$SSLCONF" ]]; then - SSLCONF="$WORKING_DIR/openssl.cnf" - write_openssl_conf "$SSLCONF" - fi - write_getsslD_template "$WORKING_DIR/getsslD.cfg" - fi - # If domain and domain config don't exist then create them. - if [[ ! -d "$DOMAIN_DIR" ]]; then - info "Making domain directory - $DOMAIN_DIR" - mkdir -p "$DOMAIN_DIR" - fi - if [[ -s "$DOMAIN_DIR/getsslD.cfg" ]]; then - info "domain config already exists $DOMAIN_DIR/getsslD.cfg" - else - info "creating domain config file in $DOMAIN_DIR/getsslD.cfg" - # if domain has an existing cert, copy from domain and use to create defaults. - EX_CERT=$(echo \ - | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:443" 2>/dev/null \ - | openssl x509 2>/dev/null) - EX_SANS="www.${DOMAIN}" - if [[ ! -z "${EX_CERT}" ]]; then - EX_SANS=$(echo "$EX_CERT" \ - | openssl x509 -noout -text 2>/dev/null| grep "Subject Alternative Name" -A2 \ - | grep -Eo "DNS:[a-zA-Z 0-9.-]*" | sed "s@DNS:$DOMAIN@@g" | grep -v '^$' | cut -c 5-) - EX_SANS=${EX_SANS//$'\n'/','} - fi - write_domain_template "$DOMAIN_DIR/getsslD.cfg" - fi - TEMP_DIR="$DOMAIN_DIR/tmp" - # end of "-c|--create" option, so exit - graceful_exit -fi -# end of "-c|--create" option to create config file. - -# if domain directory doesn't exist, then create it. -if [[ ! -d "$DOMAIN_DIR" ]]; then - debug "Making working directory - $DOMAIN_DIR" - mkdir -p "$DOMAIN_DIR" -fi - -# define a temporary directory, and if it doesn't exist, create it. -TEMP_DIR="$DOMAIN_DIR/tmp" -if [[ ! -d "${TEMP_DIR}" ]]; then - debug "Making temp directory - ${TEMP_DIR}" - mkdir -p "${TEMP_DIR}" -fi - -# read any variables from config in domain directory -if [[ -s "$DOMAIN_DIR/getsslD.cfg" ]]; then - debug "reading config from $DOMAIN_DIR/getsslD.cfg" - # shellcheck source=/dev/null - . "$DOMAIN_DIR/getsslD.cfg" -fi - -# from SERVER_TYPE set REMOTE_PORT and REMOTE_EXTRA -set_server_type - -# check config for typical errors. -check_config - -if [[ -e "$DOMAIN_DIR/FORCE_RENEWAL" ]]; then - rm -f "$DOMAIN_DIR/FORCE_RENEWAL" || error_exit "problem deleting file $DOMAIN_DIR/FORCE_RENEWAL" - _FORCE_RENEW="true" - info "${DOMAIN}: forcing renewal (due to FORCE_RENEWAL file)" -fi - -# Obtain CA resource locations -ca_all_loc=$(curl "${CA}/directory" 2>/dev/null) -URL_new_reg=$(echo "$ca_all_loc" | grep "new-reg" | awk -F'"' '{print $4}') -URL_new_authz=$(echo "$ca_all_loc" | grep "new-authz" | awk -F'"' '{print $4}') -URL_new_cert=$(echo "$ca_all_loc" | grep "new-cert" | awk -F'"' '{print $4}') - -# if check_remote is true then connect and obtain the current certificate (if not forcing renewal) -if [[ "${CHECK_REMOTE}" == "true" ]] && [[ $_FORCE_RENEW != "true" ]]; then - debug "getting certificate for $DOMAIN from remote server" - # shellcheck disable=SC2086 - EX_CERT=$(echo \ - | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} 2>/dev/null \ - | openssl x509 2>/dev/null) - if [[ ! -z "$EX_CERT" ]]; then # if obtained a cert - if [[ -s "$CERT_FILE" ]]; then # if local exists - CERT_LOCAL=$(openssl x509 -noout -fingerprint < "$CERT_FILE" 2>/dev/null) - else # since local doesn't exist leave empty so that the domain validation will happen - CERT_LOCAL="" - fi - CERT_REMOTE=$(echo "$EX_CERT" | openssl x509 -noout -fingerprint 2>/dev/null) - if [[ "$CERT_LOCAL" == "$CERT_REMOTE" ]]; then - debug "certificate on server is same as the local cert" - else - # check if the certificate is for the right domain - EX_CERT_DOMAIN=$(echo "$EX_CERT" | openssl x509 -text \ - | sed -n -e 's/^ *Subject: .* CN=\([A-Za-z0-9.-]*\).*$/\1/p; /^ *DNS:.../ { s/ *DNS://g; y/,/\n/; p; }' \ - | sort -u | grep "^$DOMAIN\$") - if [[ "$EX_CERT_DOMAIN" == "$DOMAIN" ]]; then - # check renew-date on ex_cert and compare to local ( if local exists) - enddate_ex=$(echo "$EX_CERT" | openssl x509 -noout -enddate 2>/dev/null| cut -d= -f 2-) - enddate_ex_s=$(date_epoc "$enddate_ex") - debug "external cert has enddate $enddate_ex ( $enddate_ex_s ) " - if [[ -s "$CERT_FILE" ]]; then # if local exists - enddate_lc=$(openssl x509 -noout -enddate < "$CERT_FILE" 2>/dev/null| cut -d= -f 2-) - enddate_lc_s=$(date_epoc "$enddate_lc") - debug "local cert has enddate $enddate_lc ( $enddate_lc_s ) " - else - enddate_lc_s=0 - debug "local cert doesn't exist" - fi - if [[ "$enddate_ex_s" -eq "$enddate_lc_s" ]]; then - debug "certificates expire at the same time" - elif [[ "$enddate_ex_s" -gt "$enddate_lc_s" ]]; then - # remote has longer to expiry date than local copy. - debug "remote cert has longer to run than local cert - ignoring" - else - info "${DOMAIN}: remote cert expires sooner than local, attempting to upload from local" - copy_file_to_location "domain certificate" \ - "$CERT_FILE" \ - "$DOMAIN_CERT_LOCATION" - copy_file_to_location "private key" \ - "$DOMAIN_DIR/${DOMAIN}.key" \ - "$DOMAIN_KEY_LOCATION" - copy_file_to_location "CA certificate" "$CA_CERT" "$CA_CERT_LOCATION" - cat "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}_chain.pem" - copy_file_to_location "full pem" \ - "$TEMP_DIR/${DOMAIN}_chain.pem" \ - "$DOMAIN_CHAIN_LOCATION" - cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" > "$TEMP_DIR/${DOMAIN}_K_C.pem" - copy_file_to_location "private key and domain cert pem" \ - "$TEMP_DIR/${DOMAIN}_K_C.pem" \ - "$DOMAIN_KEY_CERT_LOCATION" - cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}.pem" - copy_file_to_location "full pem" \ - "$TEMP_DIR/${DOMAIN}.pem" \ - "$DOMAIN_PEM_LOCATION" - reload_service - fi - else - info "${DOMAIN}: Certificate on remote domain does not match, ignoring remote certificate" - fi - fi - else - info "${DOMAIN}: no certificate obtained from host" - fi - # end of .... if obtained a cert -fi -# end of .... check_remote is true then connect and obtain the current certificate - -# if there is an existing certificate file, check details. -if [[ -s "$CERT_FILE" ]]; then - debug "certificate $CERT_FILE exists" - enddate=$(openssl x509 -in "$CERT_FILE" -noout -enddate 2>/dev/null| cut -d= -f 2-) - debug "local cert is valid until $enddate" - if [[ "$enddate" != "-" ]]; then - enddate_s=$(date_epoc "$enddate") - if [[ $(date_renew) -lt "$enddate_s" ]] && [[ $_FORCE_RENEW != "true" ]]; then - issuer=$(openssl x509 -in "$CERT_FILE" -noout -issuer 2>/dev/null) - if [[ "$issuer" == *"Fake LE Intermediate"* ]] && [[ "$CA" == "https://acme-v01.api.letsencrypt.org" ]]; then - debug "upgradeing from fake cert to real" - else - info "${DOMAIN}: certificate is valid for more than $RENEW_ALLOW days (until $enddate)" - # everything is OK, so exit. - graceful_exit - fi - else - debug "${DOMAIN}: certificate needs renewal" - fi - fi -fi -# end of .... if there is an existing certificate file, check details. - -if [[ ! -t 0 ]] && [[ "$PREVENT_NON_INTERACTIVE_RENEWAL" = "true" ]]; then - errmsg="$DOMAIN due for renewal," - errmsg="${errmsg} but not completed due to PREVENT_NON_INTERACTIVE_RENEWAL=true in config" - error_exit "$errmsg" -fi - -# if not reusing priavte key, then remove the old keys -if [[ "$REUSE_PRIVATE_KEY" != "true" ]]; then - if [[ -s "$DOMAIN_DIR/${DOMAIN}.key" ]]; then - rm -f "$DOMAIN_DIR/${DOMAIN}.key" - fi - if [[ -s "$DOMAIN_DIR/${DOMAIN}.ec.key" ]]; then - rm -f "$DOMAIN_DIR/${DOMAIN}.ecs.key" - fi -fi -# create new domain keys if they don't already exist -if [[ "$DUAL_RSA_ECDSA" == "false" ]]; then - create_key "${PRIVATE_KEY_ALG}" "$DOMAIN_DIR/${DOMAIN}.key" "$DOMAIN_KEY_LENGTH" -else - create_key "rsa" "$DOMAIN_DIR/${DOMAIN}.key" "$DOMAIN_KEY_LENGTH" - create_key "${PRIVATE_KEY_ALG}" "$DOMAIN_DIR/${DOMAIN}.ec.key" "$DOMAIN_KEY_LENGTH" -fi -# End of creating domain keys. +arg_parser $* -#create SAN -if [[ -z "$SANS" ]]; then - SANLIST="subjectAltName=DNS:${DOMAIN}" -elif [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then - SANLIST="subjectAltName=DNS:${SANS//,/,DNS:}" -else - SANLIST="subjectAltName=DNS:${DOMAIN},DNS:${SANS//,/,DNS:}" -fi -debug "created SAN list = $SANLIST" - -#create CSR's -if [[ "$DUAL_RSA_ECDSA" == "false" ]]; then - create_csr "$DOMAIN_DIR/${DOMAIN}.csr" "$DOMAIN_DIR/${DOMAIN}.key" -else - create_csr "$DOMAIN_DIR/${DOMAIN}.csr" "$DOMAIN_DIR/${DOMAIN}.key" - create_csr "$DOMAIN_DIR/${DOMAIN}.ec.csr" "$DOMAIN_DIR/${DOMAIN}.ec.key" -fi - -# use account key to register with CA -# currently the code registers every time, and gets an "already registered" back if it has been. -get_signing_params "$ACCOUNT_KEY" - -if [[ "$ACCOUNT_EMAIL" ]] ; then - regjson='{"resource": "new-reg", "contact": ["mailto: '$ACCOUNT_EMAIL'"], "agreement": "'$AGREEMENT'"}' -else - regjson='{"resource": "new-reg", "agreement": "'$AGREEMENT'"}' -fi - -info "Registering account" -# send the request to the ACME server. -send_signed_request "$URL_new_reg" "$regjson" - -if [[ "$code" == "" ]] || [[ "$code" == '201' ]] ; then - info "Registered" - echo "$response" > "$TEMP_DIR/account.json" -elif [[ "$code" == '409' ]] ; then - debug "Already registered" -else - error_exit "Error registering account ... $(json_get "$response" detail)" -fi -# end of registering account with CA - -# verify each domain -info "Verify each domain" - -# loop through domains for cert ( from SANS list) -if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then - alldomains=${SANS//,/ } -else - alldomains=$(echo "$DOMAIN,$SANS" | sed "s/,/ /g") -fi -dn=0 -for d in $alldomains; do - # $d is domain in current loop, which is number $dn for ACL - info "Verifying $d" - if [[ "$USE_SINGLE_ACL" == "true" ]]; then - DOMAIN_ACL="${ACL[0]}" - else - DOMAIN_ACL="${ACL[$dn]}" - fi - - # request a challenge token from ACME server - request="{\"resource\":\"new-authz\",\"identifier\":{\"type\":\"dns\",\"value\":\"$d\"}}" - send_signed_request "$URL_new_authz" "$request" - - debug "completed send_signed_request" - # check if we got a valid response and token, if not then error exit - if [[ ! -z "$code" ]] && [[ ! "$code" == '201' ]] ; then - error_exit "new-authz error: $response" - fi - - if [[ $response_status == "valid" ]]; then - info "$d is already validated" - if [[ "$DEACTIVATE_AUTH" == "true" ]]; then - deactivate_url="$(echo "$responseHeaders" | awk ' $1 ~ "^Location" {print $2}' | tr -d "\r")" - deactivate_url_list+=" $deactivate_url " - debug "url added to deactivate list ${deactivate_url}" - debug "deactivate list is now $deactivate_url_list" - fi - # increment domain-counter - ((dn++)) - else - PREVIOUSLY_VALIDATED="false" - if [[ $VALIDATE_VIA_DNS == "true" ]]; then # set up the correct DNS token for verification - # get the dns component of the ACME response - # get the token from the dns component - token=$(json_get "$response" "token" "dns-01") - debug token "$token" - # get the uri from the dns component - uri=$(json_get "$response" "uri" "dns-01") - debug uri "$uri" - - keyauthorization="$token.$thumbprint" - debug keyauthorization "$keyauthorization" - - #create signed authorization key from token. - auth_key=$(printf '%s' "$keyauthorization" | openssl dgst -sha256 -binary \ - | openssl base64 -e \ - | tr -d '\n\r' \ - | sed -e 's:=*$::g' -e 'y:+/:-_:') - debug auth_key "$auth_key" - - debug "adding dns via command: $DNS_ADD_COMMAND $d $auth_key" - if ! eval "$DNS_ADD_COMMAND" "$d" "$auth_key" ; then - error_exit "DNS_ADD_COMMAND failed for domain $d" - fi - - # find a primary / authoritative DNS server for the domain - if [[ -z "$AUTH_DNS_SERVER" ]]; then - get_auth_dns "$d" - else - primary_ns="$AUTH_DNS_SERVER" - fi - debug primary_ns "$primary_ns" - - # make a directory to hold pending dns-challenges - if [[ ! -d "$TEMP_DIR/dns_verify" ]]; then - mkdir "$TEMP_DIR/dns_verify" - fi - - # generate a file with the current variables for the dns-challenge - cat > "$TEMP_DIR/dns_verify/$d" <<- _EOF_ - token="${token}" - uri="${uri}" - keyauthorization="${keyauthorization}" - d="${d}" - primary_ns="${primary_ns}" - auth_key="${auth_key}" - _EOF_ - - else # set up the correct http token for verification - # get the token from the http component - token=$(json_get "$response" "token" "http-01") - debug token "$token" - # get the uri from the http component - uri=$(json_get "$response" "uri" "http-01") - debug uri "$uri" - - #create signed authorization key from token. - keyauthorization="$token.$thumbprint" - debug keyauthorization "$keyauthorization" - - # save variable into temporary file - echo -n "$keyauthorization" > "$TEMP_DIR/$token" - chmod 644 "$TEMP_DIR/$token" - - # copy to token to acme challenge location - umask 0022 - IFS=\; read -r -a token_locations <<<"$DOMAIN_ACL" - for t_loc in "${token_locations[@]}"; do - debug "copying file from $TEMP_DIR/$token to ${t_loc}" - copy_file_to_location "challenge token" \ - "$TEMP_DIR/$token" \ - "${t_loc}/$token" - done - umask "$ORIG_UMASK" - - wellknown_url="${CHALLENGE_CHECK_TYPE}://$d/.well-known/acme-challenge/$token" - debug wellknown_url "$wellknown_url" - - if [[ "$SKIP_HTTP_TOKEN_CHECK" == "true" ]]; then - info "SKIP_HTTP_TOKEN_CHECK=true so not checking that token is working correctly" - else - sleep "$HTTP_TOKEN_CHECK_WAIT" - # check that we can reach the challenge ourselves, if not, then error - if [[ ! "$(curl -k --silent --location "$wellknown_url")" == "$keyauthorization" ]]; then - error_exit "for some reason could not reach $wellknown_url - please check it manually" - fi - fi - - check_challenge_completion "$uri" "$d" "$keyauthorization" - - debug "remove token from ${DOMAIN_ACL}" - IFS=\; read -r -a token_locations <<<"$DOMAIN_ACL" - for t_loc in "${token_locations[@]}"; do - if [[ "${t_loc:0:4}" == "ssh:" ]] ; then - sshhost=$(echo "${t_loc}"| awk -F: '{print $2}') - command="rm -f ${t_loc:(( ${#sshhost} + 5))}/${token:?}" - debug "running following command to remove token" - debug "ssh $sshhost ${command}" - # shellcheck disable=SC2029 - ssh "$sshhost" "${command}" 1>/dev/null 2>&1 - rm -f "${TEMP_DIR:?}/${token:?}" - elif [[ "${t_loc:0:4}" == "ftp:" ]] ; then - debug "using ftp to remove token file" - ftpuser=$(echo "${t_loc}"| awk -F: '{print $2}') - ftppass=$(echo "${t_loc}"| awk -F: '{print $3}') - ftphost=$(echo "${t_loc}"| awk -F: '{print $4}') - ftplocn=$(echo "${t_loc}"| awk -F: '{print $5}') - debug "ftp user=$ftpuser - pass=$ftppass - host=$ftphost location=$ftplocn" - ftp -n <<- EOF - open $ftphost - user $ftpuser $ftppass - cd $ftplocn - delete ${token:?} - EOF - else - rm -f "${t_loc:?}/${token:?}" - fi - done - fi - # increment domain-counter - ((dn++)) - fi -done # end of ... loop through domains for cert ( from SANS list) - -# perform validation if via DNS challenge -if [[ $VALIDATE_VIA_DNS == "true" ]]; then - # loop through dns-variable files to check if dns has been changed - for dnsfile in $TEMP_DIR/dns_verify/*; do - if [[ -e "$dnsfile" ]]; then - debug "loading DNSfile: $dnsfile" - # shellcheck source=/dev/null - . "$dnsfile" - - # check for token at public dns server, waiting for a valid response. - for ns in $primary_ns; do - debug "checking dns at $ns" - ntries=0 - check_dns="fail" - while [[ "$check_dns" == "fail" ]]; do - check_result=$(nslookup -type=txt "_acme-challenge.${d}" "${ns}" \ - | grep ^_acme|awk -F'"' '{ print $2}') - debug "expecting $auth_key" - debug "${ns} gave ... $check_result" - - if [[ "$check_result" == *"$auth_key"* ]]; then - check_dns="success" - else - if [[ $ntries -lt 100 ]]; then - ntries=$(( ntries + 1 )) - info "checking DNS at ${ns} for ${d}. Attempt $ntries/100 gave wrong result, "\ - "waiting $DNS_WAIT secs before checking again" - sleep $DNS_WAIT - else - debug "dns check failed - removing existing value" - error_exit "checking _acme-challenge.${d} gave $check_result not $auth_key" - fi - fi - done - done - fi - done - - if [[ "$DNS_EXTRA_WAIT" -gt 0 && "$PREVIOUSLY_VALIDATED" != "true" ]]; then - info "sleeping $DNS_EXTRA_WAIT seconds before asking the ACME-server to check the dns" - sleep "$DNS_EXTRA_WAIT" - fi - - # loop through dns-variable files to let the ACME server check the challenges - for dnsfile in $TEMP_DIR/dns_verify/*; do - if [[ -e "$dnsfile" ]]; then - debug "loading DNSfile: $dnsfile" - # shellcheck source=/dev/null - . "$dnsfile" - - check_challenge_completion "$uri" "$d" "$keyauthorization" - - debug "remove DNS entry" - eval "$DNS_DEL_COMMAND" "$d" "$auth_key" - # remove $dnsfile after each loop. - rm -f "$dnsfile" - fi - done -fi -# end of ... perform validation if via DNS challenge -#end of varify each domain. - -# Verification has been completed for all SANS, so request certificate. -info "Verification completed, obtaining certificate." - -#obtain the certificate. -get_certificate "$DOMAIN_DIR/${DOMAIN}.csr" \ - "$CERT_FILE" \ - "$CA_CERT" -if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then - get_certificate "$DOMAIN_DIR/${DOMAIN}.ec.csr" \ - "${CERT_FILE::-4}.ec.crt" \ - "${CA_CERT::-4}.ec.crt" -fi - -# create Archive of new certs and keys. -cert_archive - -debug "Certificates obtained and archived locally, will now copy to specified locations" - -# copy certs to the correct location (creating concatenated files as required) - -copy_file_to_location "domain certificate" "$CERT_FILE" "$DOMAIN_CERT_LOCATION" -copy_file_to_location "private key" "$DOMAIN_DIR/${DOMAIN}.key" "$DOMAIN_KEY_LOCATION" -copy_file_to_location "CA certificate" "$CA_CERT" "$CA_CERT_LOCATION" -if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then - if [[ ! -z "$DOMAIN_CERT_LOCATION" ]]; then - copy_file_to_location "ec domain certificate" \ - "${CERT_FILE::-4}.ec.crt" \ - "${DOMAIN_CERT_LOCATION::-4}.ec.crt" - fi - if [[ ! -z "$DOMAIN_KEY_LOCATION" ]]; then - copy_file_to_location "ec private key" \ - "$DOMAIN_DIR/${DOMAIN}.ec.key" \ - "${DOMAIN_KEY_LOCATION::-4}.ec.key" - fi - if [[ ! -z "$CA_CERT_LOCATION" ]]; then - copy_file_to_location "ec CA certificate" \ - "${CA_CERT::-4}.ec.crt" \ - "${CA_CERT_LOCATION::-4}.ec.crt" - fi -fi - -# if DOMAIN_CHAIN_LOCATION is not blank, then create and copy file. -if [[ ! -z "$DOMAIN_CHAIN_LOCATION" ]]; then - if [[ "$(dirname "$DOMAIN_CHAIN_LOCATION")" == "." ]]; then - to_location="${DOMAIN_DIR}/${DOMAIN_CHAIN_LOCATION}" - else - to_location="${DOMAIN_CHAIN_LOCATION}" - fi - cat "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}_chain.pem" - copy_file_to_location "full chain" "$TEMP_DIR/${DOMAIN}_chain.pem" "$to_location" - if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then - cat "${CERT_FILE::-4}.ec.crt" "${CA_CERT::-4}.ec.crt" > "$TEMP_DIR/${DOMAIN}_chain.pem.ec" - copy_file_to_location "full chain" "$TEMP_DIR/${DOMAIN}_chain.pem.ec" "${to_location}.ec" - fi -fi -# if DOMAIN_KEY_CERT_LOCATION is not blank, then create and copy file. -if [[ ! -z "$DOMAIN_KEY_CERT_LOCATION" ]]; then - if [[ "$(dirname "$DOMAIN_KEY_CERT_LOCATION")" == "." ]]; then - to_location="${DOMAIN_DIR}/${DOMAIN_KEY_CERT_LOCATION}" - else - to_location="${DOMAIN_KEY_CERT_LOCATION}" - fi - cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" > "$TEMP_DIR/${DOMAIN}_K_C.pem" - copy_file_to_location "private key and domain cert pem" "$TEMP_DIR/${DOMAIN}_K_C.pem" "$to_location" - if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then - cat "$DOMAIN_DIR/${DOMAIN}.ec.key" "${CERT_FILE::-4}.ec.crt" > "$TEMP_DIR/${DOMAIN}_K_C.pem.ec" - copy_file_to_location "private ec key and domain cert pem" "$TEMP_DIR/${DOMAIN}_K_C.pem.ec" "${to_location}.ec" - fi -fi -# if DOMAIN_PEM_LOCATION is not blank, then create and copy file. -if [[ ! -z "$DOMAIN_PEM_LOCATION" ]]; then - if [[ "$(dirname "$DOMAIN_PEM_LOCATION")" == "." ]]; then - to_location="${DOMAIN_DIR}/${DOMAIN_PEM_LOCATION}" - else - to_location="${DOMAIN_PEM_LOCATION}" - fi - cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}.pem" - copy_file_to_location "full key, cert and chain pem" "$TEMP_DIR/${DOMAIN}.pem" "$to_location" - if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then - cat "$DOMAIN_DIR/${DOMAIN}.ec.key" "${CERT_FILE::-4}.ec.crt" "${CA_CERT::-4}.ec.crt" > "$TEMP_DIR/${DOMAIN}.pem.ec" - copy_file_to_location "full ec key, cert and chain pem" "$TEMP_DIR/${DOMAIN}.pem.ec" "${to_location}.ec" - fi -fi -# end of copying certs. - -# Run reload command to restart apache / nginx or whatever system -reload_service - -# deactivate authorizations -if [[ "$DEACTIVATE_AUTH" == "true" ]]; then - debug "in deactivate list is $deactivate_url_list" - for deactivate_url in $deactivate_url_list; do - resp=$(curl "$deactivate_url" 2>/dev/null) - d=$(json_get "$resp" "hostname") - info "deactivating domain $d" - debug "deactivating $deactivate_url" - send_signed_request "$deactivate_url" "{\"resource\": \"authz\", \"status\": \"deactivated\"}" - # check response - if [[ "$code" == "200" ]]; then - debug "Authorization deactivated" - else - error_exit "$g_domain: Deactivation error: $code" - fi - done -fi -# end of deactivating authorizations +} -# Check if the certificate is installed correctly -if [[ ${CHECK_REMOTE} == "true" ]]; then - sleep "$CHECK_REMOTE_WAIT" - # shellcheck disable=SC2086 - CERT_REMOTE=$(echo \ - | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} 2>/dev/null \ - | openssl x509 -noout -fingerprint 2>/dev/null) - CERT_LOCAL=$(openssl x509 -noout -fingerprint < "$CERT_FILE" 2>/dev/null) - if [[ "$CERT_LOCAL" == "$CERT_REMOTE" ]]; then - info "${DOMAIN} - certificate installed OK on server" - else - error_exit "${DOMAIN} - certificate obtained but certificate on server is different from the new certificate" - fi +# Only run main if we are not testing. +if [[ "${GETSSLD_TEST}" != true ]]; then + main $@ fi -# end of Check if the certificate is installed correctly - -# To have reached here, a certificate should have been successfully obtained. -# Use echo rather than info so that 'quiet' is ignored. -echo "certificate obtained for ${DOMAIN}" - -# gracefully exit ( tidying up temporary files etc). -graceful_exit From 84b7e13a2a708f0c7b1ed5767fccea09b488e657 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 14:15:49 -0800 Subject: [PATCH 28/58] Move logic for key creation to argparser. Refactor code and remove account specific key creation function. Signed-off-by: Dan Schaper --- getsslD | 152 +++++++++++++++++++++++++++++++++----------------------- 1 file changed, 91 insertions(+), 61 deletions(-) diff --git a/getsslD b/getsslD index 831fa0a..002e661 100755 --- a/getsslD +++ b/getsslD @@ -17,77 +17,71 @@ # more details. PROGNAME=getsslD -VERSION="1.0" +VERSION="0.2" # Default values, accepts environment variables if set, otherwise default are used WORKING_DIR=${WORKING_DIR:="/ssl}" +ACCOUNT_KEY_LOCATION=${ACCOUNT_KEY_LOCATION:="$WORKING_DIR/account.key"} ACCOUNT_KEY_LENGTH=${ACCOUNT_KEY_LENGTH:="4096"} ACCOUNT_KEY_TYPE=${ACCOUNT_KEY_TYPE:="rsa"} -ACCOUNT_KEY=${ACCOUNT_KEY:="$WORKING_DIR/account.key"} + ##### # Functions ##### -create_account_key() { - # Create account key - - # Set values to args otherwise use environment variables - # https://stackoverflow.com/a/13864829 - if [[ ! -z ${1+x} ]]; then - ACCOUNT_KEY_LENGTH="${1}" - fi - if [[ ! -z ${2+x} ]]; then - ACCOUNT_KEY="${2}" - fi - if [[ -s ${ACCOUNT_KEY} ]]; then - printf '%s\n' "Account key exists at ${ACCOUNT_KEY} skipping generation." - return 0 - elif [[ ! -d $(dirname "${2}") ]]; then - print_error "Directory for storing ${2} does not exist." - return 1 - else - printf '%s\n' "Creating account key ${ACCOUNT_KEY}:" - create_key "${ACCOUNT_KEY}" "${ACCOUNT_KEY_LENGTH}" - fi - return 0 -} create_key() { # Create an openSSL key local key_loc=${1} local key_len=${2} - local key_type + local key_type=${3} + local valid_key_type - # Determine key type by length - # Valid Let's Encrypt RSA key lengths 2048-4096 - # Valid Let's Encrypt ECC key lengths 256, 384, 521*(Not implemented) - - if [[ "${key_len}" -ge 2048 ]] && [[ "${key_len}" -le 4096 ]]; then - key_type="RSA" - elif [[ "${key_len}" -eq 256 ]]; then - key_type="prime256v1" - elif [[ "${key_len}" -eq 384 ]]; then - key_type="secp384r1" - elif [[ "${key_len}" -eq 521 ]]; then - key_type="secp521r1" + # Check for existing key + if [[ -s "${key_loc}" ]]; then + printf 'Key exists at %s skipping generation.\n' "${key_loc}" + return 0 + elif [[ ! -d $(dirname "${key_loc}") ]]; then + print_error "Directory for storing ${key_loc} does not exist." + return 1 else + printf 'Creating %s bit %s account key in %s...' "${key_len}" "${key_type}" "${key_loc}" + fi + + # Determine key type by length + # Valid Let's Encrypt RSA key lengths 2048-8192 + # Valid Let's Encrypt ECC key lengths 256, 384, 521 + + if [[ "${key_len}" -ge 2048 ]] && [[ "${key_len}" -le 8192 ]] && [[ "${key_type}" == "rsa" ]]; then + valid_key_type="RSA" + fi + + if [[ "${key_type}" == "ecc" ]]; then + if [[ "${key_len}" -eq 256 ]] ; then + valid_key_type="prime256v1" + elif [[ "${key_len}" -eq 384 ]]; then + valid_key_type="secp384r1" + elif [[ "${key_len}" -eq 521 ]]; then + valid_key_type="secp521r1" + fi + fi + + if [[ -z ${valid_key_type+x} ]]; then print_error "Invalid key length. Please check your configuration." return 1 fi - case "$key_type" in + case "$valid_key_type" in RSA) - printf '\t%s' "Creating ${key_len} bit RSA key..." openssl genrsa -out "${key_loc}" "${key_len}" >& /dev/null printf '%s\n' "Done." return 0 ;; prime256v1|secp384r1|secp521r1) - printf '\t%s' "Creating ${key_len} bit ECC key..." - openssl ecparam -genkey -out "${key_loc}" -name "${key_type}" >& /dev/null + openssl ecparam -genkey -out "${key_loc}" -name "${valid_key_type}" >& /dev/null printf '%s\n' "Done." return 0 ;; @@ -105,18 +99,21 @@ get_date() { echo $(date -u +"%Y-%m-%dT%H:%M:%SZ") } -help_message() { +help_message_top() { # Print help message cat <<- _EOF_ - Usage: "${PROGNAME}" [OPTION]... [ARGS]... + Usage: "${PROGNAME}" [option] [COMMAND] [ARGS...] Obtain SSL certificates from the letsencrypt.org ACME server. + Commands: + account Create or modify Lets Encrypt account. + Options to long options apply to short options also. Options: - -a, --account [LENGTH] [FILE] Create an account key of LENGTH with FILE name. - LENGTH is 2048-4096 for RSA keys, 256|384|521 for ECC keys. - Defaults to 4096 bit RSA key in account.key. + -r, --rsa Use RSA algorith for key generation + -e, --ecc Use elliptic curve algorithm for key or cert generation + _EOF_ return 0 } @@ -146,28 +143,62 @@ print_error() { arg_parser() { # Check CLI arguments and process + local key_type + local key_length while [[ -n ${1} ]]; do - case $1 in - -a | --account) + case ${1} in + -r | --rsa) shift - create_account_key $* - exit 0 + key_type="rsa" + ;; + -e | --ecc) + shift + key_type="ecc" ;; -h | --help) - help_message; + help_message_top exit 0 ;; + -v | --version) + printf '%s version %s\n' ${PROGNAME} ${VERSION} + exit 0 + ;; + account) + # Remove account command + shift + case $1 in + key) + # Remove key subcommand + shift + # If no key type specified on the command line + # https://stackoverflow.com/a/13864829 + if [[ -z "${key_type+x}" ]]; then + # No key types specified use default of RSA or environment variable + key_type="${ACCOUNT_KEY_TYPE:=rsa}" + printf 'No key type specified, using default of %s\n' "${key_type}" + fi + # We have a key type need length + # If no key length specified on the command line + if [[ -z "${1}" ]]; then + # No length specified, use default of 4096 or environment variable + key_length="${ACCOUNT_KEY_LENGTH:=4096}" + printf 'No key length specified, using default of %s\n' "${key_length}" + else + key_length="${1}" + fi + + create_key "${ACCOUNT_KEY_LOCATION}" "${key_length}" "${key_type}" + exit 0 + ;; + esac # End account subcommands + ;; *) - printf '%s\n\n' "Invalid option." - help_message + printf 'Invalid command\n\n' + help_message_top exit 1 ;; - esac - shift - if [[ -z ${1} ]]; then - break - fi + esac # End options done } @@ -179,8 +210,7 @@ main() { # read any variables from config in working directory if [[ -s "$WORKING_DIR/getsslD.cfg" ]]; then - debug "reading config from $WORKING_DIR/getsslD.cfg" - # shellcheck source=/dev/null + printf '%s\n'"Reading config from from $WORKING_DIR/getsslD.cfg" . "$WORKING_DIR/getsslD.cfg" fi From cd8d5b8d8ea6c9ab08a3f930b7310ae063681d08 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 14:29:14 -0800 Subject: [PATCH 29/58] Account command help. Signed-off-by: Dan Schaper --- getsslD | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/getsslD b/getsslD index 002e661..db6a92f 100755 --- a/getsslD +++ b/getsslD @@ -100,7 +100,6 @@ get_date() { } help_message_top() { - # Print help message cat <<- _EOF_ Usage: "${PROGNAME}" [option] [COMMAND] [ARGS...] @@ -111,13 +110,26 @@ help_message_top() { Options to long options apply to short options also. Options: - -r, --rsa Use RSA algorith for key generation + -r, --rsa Use RSA algorith for key generation (Default) -e, --ecc Use elliptic curve algorithm for key or cert generation _EOF_ return 0 } +help_message_account() { + + cat <<- _EOF_ + Usage: "${PROGNAME}" account [COMMAND] [ARGS...] + Manage Lets Encrypt account + + Commands: + key [LENGTH] Create Lets Encrypt account key. (Default 4096 bits) + + _EOF_ + return 0 +} + prep_workdir() { # Prepare working directory for key/cert functions @@ -191,6 +203,10 @@ arg_parser() { create_key "${ACCOUNT_KEY_LOCATION}" "${key_length}" "${key_type}" exit 0 ;; + -h | --help) + help_message_account + exit 0 + ;; esac # End account subcommands ;; *) From 9444e691a6e80a28069bb6032fbee10868a6e34e Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 17:18:47 -0800 Subject: [PATCH 30/58] Refactoring and framing skeleton. Commands follow noun verb format. Signed-off-by: Dan Schaper --- getsslD | 182 +++++++++++++++++++++++++++++++------------------------- 1 file changed, 101 insertions(+), 81 deletions(-) diff --git a/getsslD b/getsslD index db6a92f..52f235b 100755 --- a/getsslD +++ b/getsslD @@ -17,7 +17,7 @@ # more details. PROGNAME=getsslD -VERSION="0.2" +VERSION="0.2 commit cd8d5b8" # Default values, accepts environment variables if set, otherwise default are used WORKING_DIR=${WORKING_DIR:="/ssl}" @@ -31,66 +31,63 @@ ACCOUNT_KEY_TYPE=${ACCOUNT_KEY_TYPE:="rsa"} # Functions ##### - create_key() { # Create an openSSL key - local key_loc=${1} - local key_len=${2} - local key_type=${3} + local key_loc=$1 + local key_len=$2 + local key_type=$3 local valid_key_type # Check for existing key - if [[ -s "${key_loc}" ]]; then - printf 'Key exists at %s skipping generation.\n' "${key_loc}" + if [[ -s "$key_loc" ]]; then + printf 'Key exists at %s skipping generation.\n' "$key_loc" 1>&2 return 0 - elif [[ ! -d $(dirname "${key_loc}") ]]; then - print_error "Directory for storing ${key_loc} does not exist." + elif [[ ! -d $(dirname "$key_loc") ]]; then + printf 'Directory for storing $key_loc does not exist.' 1>&2 return 1 - else - printf 'Creating %s bit %s account key in %s...' "${key_len}" "${key_type}" "${key_loc}" fi # Determine key type by length # Valid Let's Encrypt RSA key lengths 2048-8192 # Valid Let's Encrypt ECC key lengths 256, 384, 521 - if [[ "${key_len}" -ge 2048 ]] && [[ "${key_len}" -le 8192 ]] && [[ "${key_type}" == "rsa" ]]; then + if [[ "$key_len" -ge 2048 ]] && [[ "$key_len" -le 8192 ]] && [[ "$key_type" == "rsa" ]]; then valid_key_type="RSA" fi - if [[ "${key_type}" == "ecc" ]]; then - if [[ "${key_len}" -eq 256 ]] ; then + if [[ "$key_type" == "ecc" ]]; then + if [[ "$key_len" -eq 256 ]] ; then valid_key_type="prime256v1" - elif [[ "${key_len}" -eq 384 ]]; then + elif [[ "$key_len" -eq 384 ]]; then valid_key_type="secp384r1" - elif [[ "${key_len}" -eq 521 ]]; then + elif [[ "$key_len" -eq 521 ]]; then valid_key_type="secp521r1" fi fi if [[ -z ${valid_key_type+x} ]]; then - print_error "Invalid key length. Please check your configuration." + printf "Invalid key length. Please check your configuration." 1>&2 return 1 fi case "$valid_key_type" in RSA) - openssl genrsa -out "${key_loc}" "${key_len}" >& /dev/null + openssl genrsa -out "$key_loc" "$key_len" >& /dev/null printf '%s\n' "Done." return 0 ;; prime256v1|secp384r1|secp521r1) - openssl ecparam -genkey -out "${key_loc}" -name "${valid_key_type}" >& /dev/null + openssl ecparam -genkey -out "$key_loc" -name "$valid_key_type" >& /dev/null printf '%s\n' "Done." return 0 ;; esac # Error inside case statement openssl generation - print_error "Error creating OpenSSL key, deleting key..." - rm "${key_loc}" - print_error "Done.\n" + printf "Error creating OpenSSL key, deleting key..." 1>&2 + rm "$key_loc" + printf "Done.\n" 1>&2 return 1 } @@ -101,8 +98,8 @@ get_date() { help_message_top() { - cat <<- _EOF_ - Usage: "${PROGNAME}" [option] [COMMAND] [ARGS...] + cat <<- _EOL_ + Usage: "$PROGNAME" [option] [COMMAND] [ARGS...] Obtain SSL certificates from the letsencrypt.org ACME server. Commands: @@ -110,33 +107,32 @@ help_message_top() { Options to long options apply to short options also. Options: - -r, --rsa Use RSA algorith for key generation (Default) - -e, --ecc Use elliptic curve algorithm for key or cert generation + -v, --version Display $PROGNAME version information. - _EOF_ + _EOL_ return 0 } help_message_account() { - cat <<- _EOF_ - Usage: "${PROGNAME}" account [COMMAND] [ARGS...] + cat <<- _EOL_ + Usage: "$PROGNAME" account [COMMAND] [ARGS...] Manage Lets Encrypt account Commands: - key [LENGTH] Create Lets Encrypt account key. (Default 4096 bits) + key Manage Lets Encrypt account key. - _EOF_ + _EOL_ return 0 } prep_workdir() { # Prepare working directory for key/cert functions - if [[ ! -d "${WORKING_DIR}" ]]; then - printf '%s' "Creating getsslD certificate storage directory - ${WORKING_DIR}..." - if ! mkdir -p "${WORKING_DIR}" >& /dev/null; then - print_error "Could not create ${WORKING_DIR}. Check volumes.\n" + if [[ ! -d "$WORKING_DIR" ]]; then + printf '%s' "Creating getsslD certificate storage directory - $WORKING_DIR..." + if ! mkdir -p "$WORKING_DIR" >& /dev/null; then + printf "!! Could not create $WORKING_DIR. Check volumes." 1>&2 exit 1 else printf '%s\n' "Done." @@ -149,7 +145,7 @@ print_error() { # Output error messages to STDERR local error=$1 - printf '!! %s\n' "${1}" 1>&2 + printf '!! %s\n' "$1" 1>&2 return 0 } @@ -158,63 +154,74 @@ arg_parser() { local key_type local key_length - while [[ -n ${1} ]]; do - case ${1} in - -r | --rsa) - shift - key_type="rsa" - ;; - -e | --ecc) - shift - key_type="ecc" - ;; - -h | --help) + while [[ ! -z ${1+x} ]]; do + case $1 in + -h | --help | "") help_message_top exit 0 ;; - -v | --version) - printf '%s version %s\n' ${PROGNAME} ${VERSION} - exit 0 - ;; account) - # Remove account command shift - case $1 in + case $1 in # account subcommand + -h | --help | "") + help_message_account + exit 0 + ;; key) - # Remove key subcommand shift - # If no key type specified on the command line - # https://stackoverflow.com/a/13864829 - if [[ -z "${key_type+x}" ]]; then - # No key types specified use default of RSA or environment variable - key_type="${ACCOUNT_KEY_TYPE:=rsa}" - printf 'No key type specified, using default of %s\n' "${key_type}" - fi - # We have a key type need length - # If no key length specified on the command line - if [[ -z "${1}" ]]; then - # No length specified, use default of 4096 or environment variable - key_length="${ACCOUNT_KEY_LENGTH:=4096}" - printf 'No key length specified, using default of %s\n' "${key_length}" - else - key_length="${1}" - fi - - create_key "${ACCOUNT_KEY_LOCATION}" "${key_length}" "${key_type}" - exit 0 + case $1 in # key subcommand + -h | --help | "") + help_message_account_key + exit 0 + ;; + create) + shift + case $1 in # create subcommand + -h | --help | "") + help_message_account_key_create + exit 0 + ;; + r | rsa) + shift + key_type="rsa" + printf 'Creating %s bit RSA account key...' $1 + create_key $ACCOUNT_KEY_LOCATION $1 $key_type + shift + ;; + e | ecc) + shift + key_type="ecc" + printf 'Creating %s bit ECC account key...' $1 + create_key $ACCOUNT_KEY_LOCATION $1 $key_type + shift + ;; + *) + printf 'Invalid command\n\n' + help_message_account_key_create + exit 1 + ;; + esac # End create subcommand + ;; + *) + printf 'Invalid command\n\n' + help_message_account_key + exit 1 + ;; + esac # End key subcommands ;; - -h | --help) + *) + printf 'Invalid command\n\n' help_message_account - exit 0 + exit 1 ;; - esac # End account subcommands - ;; + esac # End account subcommands + ;; *) printf 'Invalid command\n\n' help_message_top exit 1 ;; - esac # End options + esac # End main program done } @@ -224,10 +231,23 @@ arg_parser() { main() { +if [[ "$1" == "-v" ]] || [[ "$1" == "--version" ]]; then + printf '%s v%s\n' "$PROGNAME" "$VERSION" + exit 0 +fi + +if [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]] || [[ "$1" == "" ]]; then + help_message_top + exit 0 +fi + # read any variables from config in working directory if [[ -s "$WORKING_DIR/getsslD.cfg" ]]; then - printf '%s\n'"Reading config from from $WORKING_DIR/getsslD.cfg" - . "$WORKING_DIR/getsslD.cfg" + printf 'Reading config from from %s/getsslD.cfg\n' "$WORKING_DIR" + source "$WORKING_DIR/getsslD.cfg" +else + printf "!! Unable to find $WORKING_DIR/getsslD.cfg. Please generate or mount directory with file location." 1>&2 + exit 1 fi arg_parser $* @@ -235,6 +255,6 @@ arg_parser $* } # Only run main if we are not testing. -if [[ "${GETSSLD_TEST}" != true ]]; then +if [[ "$GETSSLD_TEST" != true ]]; then main $@ fi From 2a21cdcf5366d3e8f212ae525477e6a90e0874ff Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 17:54:55 -0800 Subject: [PATCH 31/58] Shellcheck Signed-off-by: Dan Schaper --- getsslD | 78 ++++++++++++++++++++++++++++++--------------------------- 1 file changed, 41 insertions(+), 37 deletions(-) diff --git a/getsslD b/getsslD index 52f235b..820ddd8 100755 --- a/getsslD +++ b/getsslD @@ -16,8 +16,11 @@ # GNU General Public License at for # more details. +# shellcheck disable=SC2140,SC2169 +# shellcheck shell=dash + PROGNAME=getsslD -VERSION="0.2 commit cd8d5b8" +VERSION="0.2 commit 9444e69" # Default values, accepts environment variables if set, otherwise default are used WORKING_DIR=${WORKING_DIR:="/ssl}" @@ -44,15 +47,15 @@ create_key() { printf 'Key exists at %s skipping generation.\n' "$key_loc" 1>&2 return 0 elif [[ ! -d $(dirname "$key_loc") ]]; then - printf 'Directory for storing $key_loc does not exist.' 1>&2 + printf 'Directory for storing %s does not exist.' "$key_loc" 1>&2 return 1 fi # Determine key type by length - # Valid Let's Encrypt RSA key lengths 2048-8192 - # Valid Let's Encrypt ECC key lengths 256, 384, 521 + # Valid Lets Encrypt RSA key lengths 2048-8192 + # Valid Lets Encrypt ECC key lengths 256, 384, 521 - if [[ "$key_len" -ge 2048 ]] && [[ "$key_len" -le 8192 ]] && [[ "$key_type" == "rsa" ]]; then + if [[ "$key_len" -ge "2048" ]] && [[ "$key_len" -le "8192" ]] && [[ "$key_type" == "rsa" ]]; then valid_key_type="RSA" fi @@ -66,8 +69,8 @@ create_key() { fi fi - if [[ -z ${valid_key_type+x} ]]; then - printf "Invalid key length. Please check your configuration." 1>&2 + if [[ -z "${valid_key_type+x}" ]]; then + printf 'Invalid key length. Please check your configuration.' 1>&2 return 1 fi @@ -85,9 +88,9 @@ create_key() { esac # Error inside case statement openssl generation - printf "Error creating OpenSSL key, deleting key..." 1>&2 + printf 'Error creating OpenSSL key, deleting key...' 1>&2 rm "$key_loc" - printf "Done.\n" 1>&2 + printf 'Done.\n' 1>&2 return 1 } @@ -99,7 +102,7 @@ get_date() { help_message_top() { cat <<- _EOL_ - Usage: "$PROGNAME" [option] [COMMAND] [ARGS...] + Usage: $PROGNAME [option] [COMMAND] [ARGS...] Obtain SSL certificates from the letsencrypt.org ACME server. Commands: @@ -116,7 +119,7 @@ help_message_top() { help_message_account() { cat <<- _EOL_ - Usage: "$PROGNAME" account [COMMAND] [ARGS...] + Usage: $PROGNAME account [COMMAND] [ARGS...] Manage Lets Encrypt account Commands: @@ -126,13 +129,13 @@ help_message_account() { return 0 } -prep_workdir() { +prep_workdir() { ## DAN FIX THIS # Prepare working directory for key/cert functions if [[ ! -d "$WORKING_DIR" ]]; then printf '%s' "Creating getsslD certificate storage directory - $WORKING_DIR..." if ! mkdir -p "$WORKING_DIR" >& /dev/null; then - printf "!! Could not create $WORKING_DIR. Check volumes." 1>&2 + printf '!! Could not create %s. Check volumes.' "$WORKING_DIR" 1>&2 exit 1 else printf '%s\n' "Done." @@ -141,12 +144,16 @@ prep_workdir() { return 0 } -print_error() { - # Output error messages to STDERR - local error=$1 - - printf '!! %s\n' "$1" 1>&2 - return 0 +read_config() { + # read any variables from config in working directory + if [[ -s "$WORKING_DIR/getsslD.cfg" ]]; then + printf 'Reading config from from %s/getsslD.cfg\n' "$WORKING_DIR" + # shellcheck source=/dev/null + . "$WORKING_DIR/getsslD.cfg" + else + printf '!! Unable to find %s/getsslD.cfg. Please generate or mount directory with file location.' "$WORKING_DIR" 1>&2 + exit 1 + fi } arg_parser() { @@ -154,7 +161,7 @@ arg_parser() { local key_type local key_length - while [[ ! -z ${1+x} ]]; do + while [[ ! -z "${1+x}" ]]; do case $1 in -h | --help | "") help_message_top @@ -162,21 +169,23 @@ arg_parser() { ;; account) shift - case $1 in # account subcommand + read_config + prep_workdir + case "$1" in # account subcommand -h | --help | "") help_message_account exit 0 ;; key) shift - case $1 in # key subcommand + case "$1" in # key subcommand -h | --help | "") help_message_account_key exit 0 ;; create) shift - case $1 in # create subcommand + case "$1" in # create subcommand -h | --help | "") help_message_account_key_create exit 0 @@ -184,15 +193,17 @@ arg_parser() { r | rsa) shift key_type="rsa" - printf 'Creating %s bit RSA account key...' $1 - create_key $ACCOUNT_KEY_LOCATION $1 $key_type + key_length="$1" + printf 'Creating %s bit RSA account key...' "$key_length" + create_key "$ACCOUNT_KEY_LOCATION" "$key_length" "$key_type" shift ;; e | ecc) shift key_type="ecc" - printf 'Creating %s bit ECC account key...' $1 - create_key $ACCOUNT_KEY_LOCATION $1 $key_type + key_length="$1" + printf 'Creating %s bit ECC account key...' "$key_length" + create_key "ACCOUNT_KEY_LOCATION" "$key_length" "$key_type" shift ;; *) @@ -241,20 +252,13 @@ if [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]] || [[ "$1" == "" ]]; then exit 0 fi -# read any variables from config in working directory -if [[ -s "$WORKING_DIR/getsslD.cfg" ]]; then - printf 'Reading config from from %s/getsslD.cfg\n' "$WORKING_DIR" - source "$WORKING_DIR/getsslD.cfg" -else - printf "!! Unable to find $WORKING_DIR/getsslD.cfg. Please generate or mount directory with file location." 1>&2 - exit 1 -fi +printf '%s' $get_date -arg_parser $* +arg_parser "$@" } # Only run main if we are not testing. if [[ "$GETSSLD_TEST" != true ]]; then - main $@ + main "$@" fi From 79f442e45605322fdc293522a12718a9d5221499 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 19:08:05 -0800 Subject: [PATCH 32/58] Start testing environment --- docker-compose.yml | 18 ++++++++++++++++++ test/bats.sh | 11 +++++++++++ 2 files changed, 29 insertions(+) create mode 100644 docker-compose.yml create mode 100644 test/bats.sh diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..69bc65b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +version: '2' + +services: + development: + build: + context: . + volumes: + - .:/opt/src/getssld + - /tmp/testing:/ssl + entrypoint: /bin/ash + + bats: + build: + context: ../../bats-core/ + image: bats:latest + volumes: + - .:/code + command: /code/test/bats.sh diff --git a/test/bats.sh b/test/bats.sh new file mode 100644 index 0000000..5683371 --- /dev/null +++ b/test/bats.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bats + +@test "addition using bc" { + result="$(echo 2+2 | bc)" + [ "$result" -eq 4 ] +} + +@test "addition using dc" { + result="$(echo 2 2+p | dc)" + [ "$result" -eq 4 ] +} From e519fd6d6c565d0a5f547b2a05436f1ccfb55e70 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 19:13:31 -0800 Subject: [PATCH 33/58] Initial Circleci config test --- .circleci/config.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..7a6d4ef --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,13 @@ +versio: 2 +jobs: + build: + docker: + - image: debian:stretch + steps: + - checkout + - run: + name: Greeting + command: echo Hello, world. + - run: + name: Print the Current Time + command: date From 337c9b76d64ee7bd6b5f1ec2da237a8a667a2f29 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 19:35:16 -0800 Subject: [PATCH 34/58] Circleci base image build for Alpine 3.7 Signed-off-by: Dan Schaper --- .circleci/Dockerfile | 8 ++++++++ .circleci/config.yml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 .circleci/Dockerfile diff --git a/.circleci/Dockerfile b/.circleci/Dockerfile new file mode 100644 index 0000000..01d9656 --- /dev/null +++ b/.circleci/Dockerfile @@ -0,0 +1,8 @@ +FROM alpine:3.7 + +RUN apk add --no-cache --virtual .circleci-deps \ + ca-certificates \ + git \ + gzip \ + openssh \ + tar diff --git a/.circleci/config.yml b/.circleci/config.yml index 7a6d4ef..63ba1e0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ versio: 2 jobs: build: docker: - - image: debian:stretch + - image: djschaper/circleci:1.0-alpine3.7 steps: - checkout - run: From 6c99f3b1a704e72cf31c11fe07d9316fa40ed13b Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 19:52:16 -0800 Subject: [PATCH 35/58] Install Alpine run time dependencies in to container. --- .circleci/config.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 63ba1e0..0cc6a25 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,10 +4,10 @@ jobs: docker: - image: djschaper/circleci:1.0-alpine3.7 steps: - - checkout - run: - name: Greeting - command: echo Hello, world. - - run: - name: Print the Current Time - command: date + name: Install Run Dependencies + command: | + apk add --no-cache --virtual .run-deps \ + curl \ + drill \ + openssl From 48637bfb5aea55cb5fbdec71fb3235716ed814eb Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 19:56:10 -0800 Subject: [PATCH 36/58] Clone getsslD repo and test script bare run. --- .circleci/config.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0cc6a25..56118fd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -11,3 +11,12 @@ jobs: curl \ drill \ openssl + - run: + name: Clone getsslD repository + command: | + git clone https://github.com/dschaper/getsslD.git /bin + + - run: + name: Test initial script run + environment: PATH=/bin:$PATH + command: getsslD From 7466869a919e125a3df5f60f6ab32cf2591d2ae7 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 19:57:35 -0800 Subject: [PATCH 37/58] Fix string to map for environment --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 56118fd..d34d758 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -18,5 +18,6 @@ jobs: - run: name: Test initial script run - environment: PATH=/bin:$PATH + environment: + - PATH=/bin:$PATH command: getsslD From e495d0b0c4130854c7565d8b1c3b5aff979e90a5 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 19:58:35 -0800 Subject: [PATCH 38/58] Fix string to map for environment --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d34d758..82fd8f4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,5 +19,5 @@ jobs: - run: name: Test initial script run environment: - - PATH=/bin:$PATH + - PATH: /bin:$PATH command: getsslD From 44c1c438532a07f1b880bdcd267039723d71366b Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 19:59:37 -0800 Subject: [PATCH 39/58] Create directory to clone into --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 82fd8f4..6c65ee6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,10 +14,10 @@ jobs: - run: name: Clone getsslD repository command: | - git clone https://github.com/dschaper/getsslD.git /bin + git clone https://github.com/dschaper/getsslD.git /bin/getsslD - run: name: Test initial script run environment: - - PATH: /bin:$PATH + - PATH: /bin/getsslD:$PATH command: getsslD From c00c97db7ca5d92ae4f386c5ed842d594981a182 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 20:05:30 -0800 Subject: [PATCH 40/58] Use CircleCi checkout command instead of manual clone --- .circleci/config.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6c65ee6..3ac3b1f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -11,13 +11,8 @@ jobs: curl \ drill \ openssl - - run: - name: Clone getsslD repository - command: | - git clone https://github.com/dschaper/getsslD.git /bin/getsslD - + - checkout - run: name: Test initial script run - environment: - - PATH: /bin/getsslD:$PATH + shell: /bin/ash command: getsslD From 89070ceaa16a385ad59fd80e8b629e1720c57bbf Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 20:12:27 -0800 Subject: [PATCH 41/58] Try out building a branch instead of master --- .circleci/config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 3ac3b1f..0c5c938 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,6 +3,8 @@ jobs: build: docker: - image: djschaper/circleci:1.0-alpine3.7 + environment: + - CIRCLE_BRANCH: integration/circleci steps: - run: name: Install Run Dependencies From d5c513a7cecba4a69da637568b3a2a56a7f89e25 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 20:19:47 -0800 Subject: [PATCH 42/58] Add project to path and try dry run. --- .circleci/config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0c5c938..24361cc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -17,4 +17,6 @@ jobs: - run: name: Test initial script run shell: /bin/ash + environment: + - PATH: /root/project:$PATH command: getsslD From 50b407569f1cea64dbcde59c9b906a355b2d0652 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 20:24:32 -0800 Subject: [PATCH 43/58] Move PATH declaration to job level --- .circleci/config.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 24361cc..915cce7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,6 +5,7 @@ jobs: - image: djschaper/circleci:1.0-alpine3.7 environment: - CIRCLE_BRANCH: integration/circleci + - PATH: /root/project:$PATH steps: - run: name: Install Run Dependencies @@ -17,6 +18,4 @@ jobs: - run: name: Test initial script run shell: /bin/ash - environment: - - PATH: /root/project:$PATH command: getsslD From 31a14dfe3496236ae284ee1fdb8f36dfd9483ed7 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 20:26:09 -0800 Subject: [PATCH 44/58] Move PATH declaration to job level --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 915cce7..e374fa1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,7 +5,7 @@ jobs: - image: djschaper/circleci:1.0-alpine3.7 environment: - CIRCLE_BRANCH: integration/circleci - - PATH: /root/project:$PATH + - PATH: $PATH:$CIRCLE_WORKING_DIRECTORY steps: - run: name: Install Run Dependencies From 9eab25728c97334f515c1bf03c72f73e0a02bc50 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 20:31:04 -0800 Subject: [PATCH 45/58] First step on CircleCi PATH issue. --- .circleci/config.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e374fa1..0c5c938 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,7 +5,6 @@ jobs: - image: djschaper/circleci:1.0-alpine3.7 environment: - CIRCLE_BRANCH: integration/circleci - - PATH: $PATH:$CIRCLE_WORKING_DIRECTORY steps: - run: name: Install Run Dependencies From 10b87376666b7577a83d75a1fedd75c42f95d56a Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 20:39:11 -0800 Subject: [PATCH 46/58] Next try at CircleCi PATH issue. --- .circleci/config.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0c5c938..c641ba2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,6 +6,9 @@ jobs: environment: - CIRCLE_BRANCH: integration/circleci steps: + - run: + name: Set PATH in environment + command: echo 'export PATH=$CIRCLECI_WORKING_DIR:$PATH' > /etc/profile.d/ash.sh - run: name: Install Run Dependencies command: | From 58d73376e80802cace34a9d4451a182a1fdb7ad7 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 20:43:31 -0800 Subject: [PATCH 47/58] Another try at CircleCi PATH issue. --- .circleci/config.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c641ba2..3d1d336 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,7 +8,10 @@ jobs: steps: - run: name: Set PATH in environment - command: echo 'export PATH=$CIRCLECI_WORKING_DIR:$PATH' > /etc/profile.d/ash.sh + command: echo 'export PATH=$CIRCLE_WORKING_DIR:$PATH' > /etc/profile.d/ash_path + - run: + name: Export PATH to environment + command: source /etc/profile.d/ash_path - run: name: Install Run Dependencies command: | From 65e129cd0419aa6e413052118626c42f1bdfb518 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 20:46:07 -0800 Subject: [PATCH 48/58] Copy the script to path'd directory. --- .circleci/config.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 3d1d336..dc330e5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,12 +6,6 @@ jobs: environment: - CIRCLE_BRANCH: integration/circleci steps: - - run: - name: Set PATH in environment - command: echo 'export PATH=$CIRCLE_WORKING_DIR:$PATH' > /etc/profile.d/ash_path - - run: - name: Export PATH to environment - command: source /etc/profile.d/ash_path - run: name: Install Run Dependencies command: | @@ -20,6 +14,9 @@ jobs: drill \ openssl - checkout + - run: + name: Copy getsslD to path'd directory + command: cp $CIRCLE_WORKING_DIRECTORY/getsslD /bin/ - run: name: Test initial script run shell: /bin/ash From cf91b0a49b4e86cadd4c6f739a5e1efbaf98a03e Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 20:50:13 -0800 Subject: [PATCH 49/58] Copy the script to working directory and copy to path'd directory. --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index dc330e5..59f687b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,6 +5,7 @@ jobs: - image: djschaper/circleci:1.0-alpine3.7 environment: - CIRCLE_BRANCH: integration/circleci + working_directory: /srv/getsslD steps: - run: name: Install Run Dependencies @@ -16,7 +17,7 @@ jobs: - checkout - run: name: Copy getsslD to path'd directory - command: cp $CIRCLE_WORKING_DIRECTORY/getsslD /bin/ + command: cp /srv/getsslD/getsslD /bin/ - run: name: Test initial script run shell: /bin/ash From caa1adcb79f7e02e295be4baa9c90778252e8676 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 21:06:52 -0800 Subject: [PATCH 50/58] Change to koalaman/shellcheck-alpine based image. Shellcheck getsslD. --- .circleci/config.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 59f687b..7043a73 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,9 +2,10 @@ versio: 2 jobs: build: docker: - - image: djschaper/circleci:1.0-alpine3.7 + - image: djschaper/circleci:1.1-alpine3.7sc environment: - CIRCLE_BRANCH: integration/circleci + shell: /bin/ash working_directory: /srv/getsslD steps: - run: @@ -20,5 +21,7 @@ jobs: command: cp /srv/getsslD/getsslD /bin/ - run: name: Test initial script run - shell: /bin/ash command: getsslD + - run: + name: Shellcheck getsslD + command: shellcheck /srv/getsslD/getsslD From 01bfc71827b46439e59e40ce8eec826274b6dfff Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 21:11:16 -0800 Subject: [PATCH 51/58] Shellcheck fix 2 warnings. Signed-off-by: Dan Schaper --- getsslD | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/getsslD b/getsslD index 820ddd8..aaa3277 100755 --- a/getsslD +++ b/getsslD @@ -94,10 +94,10 @@ create_key() { return 1 } -get_date() { - # get current date and time in UTC YYYY-MM-DDTHH:MM:SSZ - echo $(date -u +"%Y-%m-%dT%H:%M:%SZ") -} +#get_date() { +# get current date and time in UTC YYYY-MM-DDTHH:MM:SSZ +# echo "$(date -u +"%Y-%m-%dT%H:%M:%SZ") +#} help_message_top() { @@ -252,8 +252,6 @@ if [[ "$1" == "-h" ]] || [[ "$1" == "--help" ]] || [[ "$1" == "" ]]; then exit 0 fi -printf '%s' $get_date - arg_parser "$@" } From d809418aa1d93e011c1a1a5151e33972e0453b18 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 22:07:53 -0800 Subject: [PATCH 52/58] `account key create` with no arguments uses default values. Signed-off-by: Dan Schaper --- getsslD | 46 +++++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/getsslD b/getsslD index aaa3277..2f0c7ac 100755 --- a/getsslD +++ b/getsslD @@ -16,14 +16,14 @@ # GNU General Public License at for # more details. -# shellcheck disable=SC2140,SC2169 +# shellcheck disable=SC2169 # shellcheck shell=dash PROGNAME=getsslD VERSION="0.2 commit 9444e69" # Default values, accepts environment variables if set, otherwise default are used -WORKING_DIR=${WORKING_DIR:="/ssl}" +WORKING_DIR=${WORKING_DIR:="/ssl"} ACCOUNT_KEY_LOCATION=${ACCOUNT_KEY_LOCATION:="$WORKING_DIR/account.key"} ACCOUNT_KEY_LENGTH=${ACCOUNT_KEY_LENGTH:="4096"} ACCOUNT_KEY_TYPE=${ACCOUNT_KEY_TYPE:="rsa"} @@ -37,6 +37,11 @@ ACCOUNT_KEY_TYPE=${ACCOUNT_KEY_TYPE:="rsa"} create_key() { # Create an openSSL key + if [[ "$#" -ne 3 ]]; then + printf '!! Invalid number of arguments sent to create_key function.\n' + exit 1 + fi + local key_loc=$1 local key_len=$2 local key_type=$3 @@ -51,10 +56,11 @@ create_key() { return 1 fi - # Determine key type by length # Valid Lets Encrypt RSA key lengths 2048-8192 # Valid Lets Encrypt ECC key lengths 256, 384, 521 + + if [[ "$key_len" -ge "2048" ]] && [[ "$key_len" -le "8192" ]] && [[ "$key_type" == "rsa" ]]; then valid_key_type="RSA" fi @@ -158,11 +164,11 @@ read_config() { arg_parser() { # Check CLI arguments and process - local key_type - local key_length - while [[ ! -z "${1+x}" ]]; do - case $1 in + while [[ "$#" -gt 0 ]] + do + case $1 + in -h | --help | "") help_message_top exit 0 @@ -171,22 +177,25 @@ arg_parser() { shift read_config prep_workdir - case "$1" in # account subcommand + case "$1" + in # account subcommand -h | --help | "") help_message_account exit 0 ;; key) shift - case "$1" in # key subcommand + case "$1" + in # key subcommand -h | --help | "") help_message_account_key exit 0 ;; create) shift - case "$1" in # create subcommand - -h | --help | "") + case "$1" + in # create subcommand + -h | --help) help_message_account_key_create exit 0 ;; @@ -197,15 +206,23 @@ arg_parser() { printf 'Creating %s bit RSA account key...' "$key_length" create_key "$ACCOUNT_KEY_LOCATION" "$key_length" "$key_type" shift + exit $? ;; e | ecc) shift key_type="ecc" key_length="$1" printf 'Creating %s bit ECC account key...' "$key_length" - create_key "ACCOUNT_KEY_LOCATION" "$key_length" "$key_type" + create_key "$ACCOUNT_KEY_LOCATION" "$key_length" "$key_type" shift ;; + "") + key_type=$ACCOUNT_KEY_TYPE + key_length=$ACCOUNT_KEY_LENGTH + printf 'Creating %s bit %s account key with default values...' "$key_length" "$key_type" + create_key "$ACCOUNT_KEY_LOCATION" "$key_length" "$key_type" + exit $? + ;; *) printf 'Invalid command\n\n' help_message_account_key_create @@ -255,8 +272,3 @@ fi arg_parser "$@" } - -# Only run main if we are not testing. -if [[ "$GETSSLD_TEST" != true ]]; then - main "$@" -fi From 1b08d06c5a32f4f85471f6e94793e3b199663824 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Sun, 24 Dec 2017 22:22:53 -0800 Subject: [PATCH 53/58] Add metafiles to tracking --- .circleci/Dockerfile | 2 +- Dockerfile.dev | 15 + getssl.orig | 2165 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 2181 insertions(+), 1 deletion(-) create mode 100644 Dockerfile.dev create mode 100644 getssl.orig diff --git a/.circleci/Dockerfile b/.circleci/Dockerfile index 01d9656..ed33b22 100644 --- a/.circleci/Dockerfile +++ b/.circleci/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.7 +FROM koalaman/shellcheck-alpine:v0.4.7 RUN apk add --no-cache --virtual .circleci-deps \ ca-certificates \ diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..01a7ace --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,15 @@ +FROM koalaman/shellcheck-alpine:v0.4.7 + +ENV PATH=/bin:$PATH + +WORKDIR /ssl + +RUN apk --no-cache --virtual .run-depends add \ + curl \ + drill \ + openssl + +#COPY getsslD /bin/getsslD + +ENTRYPOINT [ "getsslD" ] +CMD [ "--help" ] diff --git a/getssl.orig b/getssl.orig new file mode 100644 index 0000000..7f3713e --- /dev/null +++ b/getssl.orig @@ -0,0 +1,2165 @@ +#!/usr/bin/env bash +# --------------------------------------------------------------------------- +# getssl - Obtain SSL certificates from the letsencrypt.org ACME server + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License at for +# more details. + +# For usage, run "getssl -h" or see https://github.com/srvrco/getssl + +# Revision history: +# 2016-01-08 Created (v0.1) +# 2016-01-11 type correction and upload to github (v0.2) +# 2016-01-11 added import of any existing cert on -c option (v0.3) +# 2016-01-12 corrected formatting of imported certificate (v0.4) +# 2016-01-12 corrected error on removal of token in some instances (v0.5) +# 2016-01-18 corrected issue with removing tmp if run as root with the -c option (v0.6) +# 2016-01-18 added option to upload a single PEN file ( used by cpanel) (v0.7) +# 2016-01-23 added dns challenge option (v0.8) +# 2016-01-24 create the ACL directory if it does not exist. (v0.9) - dstosberg +# 2016-01-26 correcting a couple of small bugs and allow curl to follow redirects (v0.10) +# 2016-01-27 add a very basic openssl.cnf file if it doesn't exist and tidy code slightly (v0.11) +# 2016-01-28 Typo corrections, quoted file variables and fix bug on DNS_DEL_COMMAND (v0.12) +# 2016-01-28 changed DNS checks to use nslookup and allow hyphen in domain names (v0.13) +# 2016-01-29 Fix ssh-reload-command, extra waiting for DNS-challenge, +# 2016-01-29 add error_exit and cleanup help message (v0.14) +# 2016-01-29 added -a|--all option to renew all configured certificates (v0.15) +# 2016-01-29 added option for elliptic curve keys (v0.16) +# 2016-01-29 added server-type option to use and check cert validity from website (v0.17) +# 2016-01-30 added --quiet option for running in cron (v0.18) +# 2016-01-31 removed usage of xxd to make script more compatible across versions (v0.19) +# 2016-01-31 removed usage of base64 to make script more compatible across platforms (v0.20) +# 2016-01-31 added option to safe a full chain certificate (v0.21) +# 2016-02-01 commented code and added option for copying concatenated certs to file (v0.22) +# 2016-02-01 re-arrange flow for DNS-challenge, to reduce time taken (v0.23) +# 2016-02-04 added options for other server types (ldaps, or any port) and check_remote (v0.24) +# 2016-02-04 added short sleep following service restart before checking certs (v0.25) +# 2016-02-12 fix challenge token location when directory doesn't exist (v0.26) +# 2016-02-17 fix sed -E issue, and reduce length of renew check to 365 days for older systems (v0.27) +# 2016-04-05 Ensure DNS cleanup on error exit. (0.28) - pecigonzalo +# 2016-04-15 Remove NS Lookup of A record when using dns validation (0.29) - pecigonzalo +# 2016-04-17 Improving the wording in a couple of comments and info statements. (0.30) +# 2016-05-04 Improve check for if DNS_DEL_COMMAND is blank. (0.31) +# 2016-05-06 Setting umask to 077 for security of private keys etc. (0.32) +# 2016-05-20 update to reflect changes in staging ACME server json (0.33) +# 2016-05-20 tidying up checking of json following AMCE changes. (0.34) +# 2016-05-21 added AUTH_DNS_SERVER to getssl.cfg as optional definition of authoritative DNS server (0.35) +# 2016-05-21 added DNS_WAIT to getssl.cfg as (default = 10 seconds as before) (0.36) +# 2016-05-21 added PUBLIC_DNS_SERVER option, for forcing use of an external DNS server (0.37) +# 2016-05-28 added FTP method of uploading tokens to remote server (blocked for certs as not secure) (0.38) +# 2016-05-28 added FTP method into the default config notes. (0.39) +# 2016-05-30 Add sftp with password to copy files (0.40) +# 2016-05-30 Add version check to see if there is a more recent version of getssl (0.41) +# 2016-05-30 Add [-u|--upgrade] option to automatically upgrade getssl (0.42) +# 2016-05-30 Added backup when auto-upgrading (0.43) +# 2016-05-30 Improvements to auto-upgrade (0.44) +# 2016-05-31 Improved comments - no structural changes +# 2016-05-31 After running for nearly 6 months, final testing prior to a 1.00 stable version. (0.90) +# 2016-06-01 Reorder functions alphabetically as part of code tidy. (0.91) +# 2016-06-03 Version 1.0 of code for release (1.00) +# 2016-06-09 bugfix of issue 44, and add success statement (ignoring quiet flag) (1.01) +# 2016-06-13 test return status of DNS_ADD_COMMAND and error_exit if a problem (hadleyrich) (1.02) +# 2016-06-13 bugfix of issue 45, problem with SERVER_TYPE when it's just a port number (1.03) +# 2016-06-13 bugfix issue 47 - DNS_DEL_COMMAND cleanup was run when not required. (1.04) +# 2016-06-15 add error checking on RELOAD_CMD (1.05) +# 2016-06-20 updated sed and date functions to run on MAC OS X (1.06) +# 2016-06-20 added CHALLENGE_CHECK_TYPE variable to allow checks direct on https rather than http (1.07) +# 2016-06-21 updated grep functions to run on MAC OS X (1.08) +# 2016-06-11 updated to enable running on windows with cygwin (1.09) +# 2016-07-02 Corrections to work with older slackware issue #56 (1.10) +# 2016-07-02 Updating help info re ACL in config file (1.11) +# 2016-07-04 adding DOMAIN_STORAGE as a variable to solve for issue #59 (1.12) +# 2016-07-05 updated order to better handle non-standard DOMAIN_STORAGE location (1.13) +# 2016-07-06 added additional comments about SANS in example template (1.14) +# 2016-07-07 check for duplicate domains in domain / SANS (1.15) +# 2016-07-08 modified to be used on older bash for issue #64 (1.16) +# 2016-07-11 added -w to -a option and comments in domain template (1.17) +# 2016-07-18 remove / regenerate csr when generating new private domain key (1.18) +# 2016-07-21 add output of combined private key and domain cert (1.19) +# 2016-07-21 updated typo (1.20) +# 2016-07-22 corrected issue in nslookup debug option - issue #74 (1.21) +# 2016-07-26 add more server-types based on openssl s_client (1.22) +# 2016-08-01 updated agreement for letsencrypt (1.23) +# 2016-08-02 updated agreement for letsencrypt to update automatically (1.24) +# 2016-08-03 improve messages on test of certificate installation (1.25) +# 2016-08-04 remove carriage return from agreement - issue #80 (1.26) +# 2016-08-04 set permissions for token folders - issue #81 (1.27) +# 2016-08-07 allow default chained file creation - issue #85 (1.28) +# 2016-08-07 use copy rather than move when archiving certs - issue #86 (1.29) +# 2016-08-07 enable use of a single ACL for all checks (if USE_SINGLE_ACL="true" (1.30) +# 2016-08-23 check for already validated domains (issue #93) - (1.31) +# 2016-08-23 updated already validated domains (1.32) +# 2016-08-23 included better force_renew and template for USE_SINGLE_ACL (1.33) +# 2016-08-23 enable insecure certificate on https token check #94 (1.34) +# 2016-08-23 export OPENSSL_CONF so it's used by all openssl commands (1.35) +# 2016-08-25 updated defaults for ACME agreement (1.36) +# 2016-09-04 correct issue #101 when some domains already validated (1.37) +# 2016-09-12 Checks if which is installed (1.38) +# 2016-09-13 Don't check for updates, if -U parameter has been given (1.39) +# 2016-09-17 Improved error messages from invalid certs (1.40) +# 2016-09-19 remove update check on recursive calls when using -a (1.41) +# 2016-09-21 changed shebang for portability (1.42) +# 2016-09-21 Included option to Deactivate an Authorization (1.43) +# 2016-09-22 retry on 500 error from ACME server (1.44) +# 2016-09-22 added additional checks and retry on 500 error from ACME server (1.45) +# 2016-09-24 merged in IPv6 support (1.46) +# 2016-09-27 added additional debug info issue #119 (1.47) +# 2016-09-27 removed IPv6 switch in favour of checking both IPv4 and IPv6 (1.48) +# 2016-09-28 Add -Q, or --mute, switch to mute notifications about successfully upgrading getssl (1.49) +# 2016-09-30 improved portability to work natively on FreeBSD, Slackware and OSX (1.50) +# 2016-09-30 comment out PRIVATE_KEY_ALG from the domain template Issue #125 (1.51) +# 2016-10-03 check remote certificate for right domain before saving to local (1.52) +# 2016-10-04 allow existing CSR with domain name in subject (1.53) +# 2016-10-05 improved the check for CSR with domain in subject (1.54) +# 2016-10-06 prints update info on what was included in latest updates (1.55) +# 2016-10-06 when using -a flag, ignore folders in working directory which aren't domains (1.56) +# 2016-10-12 alllow multiple tokens in DNS challenge (1.57) +# 2016-10-14 added CHECK_ALL_AUTH_DNS option to check all DNS servres, not just one primary server (1.58) +# 2016-10-14 added archive of chain and private key for each cert, and purge old archives (1.59) +# 2016-10-17 updated info comment on failed cert due to rate limits. (1.60) +# 2016-10-17 fix error messages when using 1.0.1e-fips (1.61) +# 2016-10-20 set secure permissions when generating account key (1.62) +# 2016-10-20 set permsissions to 700 for getssl script during upgrade (1.63) +# 2016-10-20 add option to revoke a certificate (1.64) +# 2016-10-21 set revocation server default to acme-v01.api.letsencrypt.org (1.65) +# 2016-10-21 bug fix for revocation on different servers. (1.66) +# 2016-10-22 Tidy up archive code for certificates and reduce permissions for security +# 2016-10-22 Add EC signing for secp384r1 and secp521r1 (the latter not yet supported by Let's Encrypt +# 2016-10-22 Add option to create a new private key for every cert (REUSE_PRIVATE_KEY="true" by default) +# 2016-10-22 Combine EC signing, Private key reuse and archive permissions (1.67) +# 2016-10-25 added CHECK_REMOTE_WAIT option ( to pause before final remote check) +# 2016-10-25 Added EC account key support ( prime256v1, secp384r1 ) (1.68) +# 2016-10-25 Ignore DNS_EXTRA_WAIT if all domains already validated (issue #146) (1.69) +# 2016-10-25 Add option for dual ESA / EDSA certs (1.70) +# 2016-10-25 bug fix Issue #141 challenge error 400 (1.71) +# 2016-10-26 check content of key files, not just recreate if missing. +# 2016-10-26 Improvements on portability (1.72) +# 2016-10-26 Date formatting for busybox (1.73) +# 2016-10-27 bug fix - issue #157 not recognising EC keys on some versions of openssl (1.74) +# 2016-10-31 generate EC account keys and tidy code. +# 2016-10-31 fix warning message if cert doesn't exist (1.75) +# 2016-10-31 remove only specified DNS token #161 (1.76) +# 2016-11-03 Reduce long lines, and remove echo from update (1.77) +# 2016-11-05 added TOKEN_USER_ID (to set ownership of token files ) +# 2016-11-05 updated style to work with latest shellcheck (1.78) +# 2016-11-07 style updates +# 2016-11-07 bug fix DOMAIN_PEM_LOCATION starting with ./ #167 +# 2016-11-08 Fix for openssl 1.1.0 #166 (1.79) +# 2016-11-08 Add and comment optional sshuserid for ssh ACL (1.80) +# 2016-11-09 Add SKIP_HTTP_TOKEN_CHECK option (Issue #170) (1.81) +# 2016-11-13 bug fix DOMAIN_KEY_CERT generation (1.82) +# 2016-11-17 add PREVENT_NON_INTERACTIVE_RENEWAL option (1.83) +# 2016-12-03 add HTTP_TOKEN_CHECK_WAIT option (1.84) +# 2016-12-03 bugfix CSR renewal when no SANS and when using MINGW (1.85) +# 2016-12-16 create CSR_SUBJECT variable - Issue #193 +# 2016-12-16 added fullchain to archive (1.86) +# 2016-12-16 updated DOMAIN_PEM_LOCATION when using DUAL_RSA_ECDSA (1.87) +# 2016-12-19 allow user to ignore permission preservation with nfsv3 shares (1.88) +# 2016-12-19 bug fix for CA (1.89) +# 2016-12-19 included IGNORE_DIRECTORY_DOMAIN option (1.90) +# 2016-12-22 allow copying files to multiple locations (1.91) +# 2016-12-22 bug fix for copying tokens to multiple locations (1.92) +# 2016-12-23 tidy code - place default variables in alphabetical order. +# 2016-12-27 update checks to work with openssl in FIPS mode (1.93) +# 2016-12-28 fix leftover tmpfiles in upgrade routine (1.94) +# 2016-12-28 tidied up upgrade tmpfile handling (1.95) +# 2017-01-01 update comments +# 2017-01-01 create stable release 2.0 (2.00) +# 2017-01-02 Added option to limit amount of old versions to keep (2.01) +# 2017-01-03 Created check_config function to list all obvious config issues (2.02) +# 2017-01-10 force renew if FORCE_RENEWAL file exists (2.03) +# 2017-01-12 added drill, dig or host as alternatives to nslookup (2.04) +# 2017-01-18 bugfix issue #227 - error deleting csr if doesn't exist +# 2017-01-18 issue #228 check private key and account key are different (2.05) +# 2017-01-21 issue #231 mingw bugfix and typos in debug messages (2.06) +# 2017-01-29 issue #232 use neutral locale for date formatting (2.07) +# 2017-01-30 issue #243 compatibility with bash 3.0 (2.08) +# 2017-01-30 issue #243 additional compatibility with bash 3.0 (2.09) +# 2017-02-18 add OCSP Must-Staple to the domain csr generation (2.10) +# ---------------------------------------------------------------------------------------- + +PROGNAME=${0##*/} +VERSION="2.10" + +# defaults +ACCOUNT_KEY_LENGTH=4096 +ACCOUNT_KEY_TYPE="rsa" +CA="https://acme-staging.api.letsencrypt.org" +CA_CERT_LOCATION="" +CHALLENGE_CHECK_TYPE="http" +CHECK_ALL_AUTH_DNS="false" +CHECK_REMOTE="true" +CHECK_REMOTE_WAIT=0 +CODE_LOCATION="https://raw.githubusercontent.com/srvrco/getssl/master/getssl" +CSR_SUBJECT="/" +DEACTIVATE_AUTH="false" +DEFAULT_REVOKE_CA="https://acme-v01.api.letsencrypt.org" +DNS_EXTRA_WAIT="" +DNS_WAIT=10 +DOMAIN_KEY_LENGTH=4096 +DUAL_RSA_ECDSA="false" +GETSSL_IGNORE_CP_PRESERVE="false" +HTTP_TOKEN_CHECK_WAIT=0 +IGNORE_DIRECTORY_DOMAIN="false" +ORIG_UMASK=$(umask) +PREVIOUSLY_VALIDATED="true" +PRIVATE_KEY_ALG="rsa" +PUBLIC_DNS_SERVER="" +RELOAD_CMD="" +RENEW_ALLOW="30" +REUSE_PRIVATE_KEY="true" +SERVER_TYPE="https" +SKIP_HTTP_TOKEN_CHECK="false" +SSLCONF="$(openssl version -d 2>/dev/null| cut -d\" -f2)/openssl.cnf" +OCSP_MUST_STAPLE="false" +TEMP_UPGRADE_FILE="" +TOKEN_USER_ID="" +USE_SINGLE_ACL="false" +VALIDATE_VIA_DNS="" +WORKING_DIR=~/.getssl +_CHECK_ALL=0 +_CREATE_CONFIG=0 +_FORCE_RENEW=0 +_KEEP_VERSIONS="" +_MUTE=0 +_QUIET=0 +_RECREATE_CSR=0 +_REVOKE=0 +_UPGRADE=0 +_UPGRADE_CHECK=1 +_USE_DEBUG=0 +config_errors="false" +LANG=C + +# store copy of original command in case of upgrading script and re-running +ORIGCMD="$0 $*" + +# Define all functions (in alphabetical order) + +cert_archive() { # Archive certificate file by copying files to dated archive dir. + debug "creating an achive copy of current new certs" + date_time=$(date +%Y_%m_%d_%H_%M) + mkdir -p "${DOMAIN_DIR}/archive/${date_time}" + umask 077 + cp "$CERT_FILE" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.crt" + cp "$DOMAIN_DIR/${DOMAIN}.csr" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.csr" + cp "$DOMAIN_DIR/${DOMAIN}.key" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.key" + cp "$CA_CERT" "${DOMAIN_DIR}/archive/${date_time}/chain.crt" + cat "$CERT_FILE" "$CA_CERT" > "${DOMAIN_DIR}/archive/${date_time}/fullchain.crt" + if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then + cp "${CERT_FILE::-4}.ec.crt" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.ec.crt" + cp "$DOMAIN_DIR/${DOMAIN}.ec.csr" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.ec.csr" + cp "$DOMAIN_DIR/${DOMAIN}.ec.key" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.ec.key" + cp "${CA_CERT::-4}.ec.crt" "${DOMAIN_DIR}/archive/${date_time}/chain.ec.crt" + cat "${CERT_FILE::-4}.ec.crt" "${CA_CERT::-4}.ec.crt" > "${DOMAIN_DIR}/archive/${date_time}/fullchain.ec.crt" + fi + umask "$ORIG_UMASK" + debug "purging old GetSSL archives" + purge_archive "$DOMAIN_DIR" +} + +check_challenge_completion() { # checks with the ACME server if our challenge is OK + uri=$1 + domain=$2 + keyauthorization=$3 + + debug "sending request to ACME server saying we're ready for challenge" + send_signed_request "$uri" "{\"resource\": \"challenge\", \"keyAuthorization\": \"$keyauthorization\"}" + + # check response from our request to perform challenge + if [[ ! -z "$code" ]] && [[ ! "$code" == '202' ]] ; then + error_exit "$domain:Challenge error: $code" + fi + + # loop "forever" to keep checking for a response from the ACME server. + while true ; do + debug "checking" + if ! get_cr "$uri" ; then + error_exit "$domain:Verify error:$code" + fi + + status=$(json_get "$response" status) + + # If ACME response is valid, then break out of loop + if [[ "$status" == "valid" ]] ; then + info "Verified $domain" + break; + fi + + # if ACME response is that their check gave an invalid response, error exit + if [[ "$status" == "invalid" ]] ; then + err_detail=$(json_get "$response" detail) + error_exit "$domain:Verify error:$err_detail" + fi + + # if ACME response is pending ( they haven't completed checks yet) then wait and try again. + if [[ "$status" == "pending" ]] ; then + info "Pending" + else + error_exit "$domain:Verify error:$response" + fi + debug "sleep 5 secs before testing verify again" + sleep 5 + done + + if [[ "$DEACTIVATE_AUTH" == "true" ]]; then + deactivate_url=$(echo "$responseHeaders" | grep "^Link" | awk -F"[<>]" '{print $2}') + deactivate_url_list="$deactivate_url_list $deactivate_url" + debug "adding url to deactivate list - $deactivate_url" + fi +} + +check_config() { # check the config files for all obvious errors + debug "checking config" + + # check keys + case "$ACCOUNT_KEY_TYPE" in + rsa|prime256v1|secp384r1|secp521r1) + debug "checked ACCOUNT_KEY_TYPE " ;; + *) + info "${DOMAIN}: invalid ACCOUNT_KEY_TYPE - $ACCOUNT_KEY_TYPE" + config_errors=true ;; + esac + if [[ "$ACCOUNT_KEY" == "$DOMAIN_DIR/${DOMAIN}.key" ]]; then + info "${DOMAIN}: ACCOUNT_KEY and domain key ( $DOMAIN_DIR/${DOMAIN}.key ) must be different" + config_errors=true + fi + case "$PRIVATE_KEY_ALG" in + rsa|prime256v1|secp384r1|secp521r1) + debug "checked PRIVATE_KEY_ALG " ;; + *) + info "${DOMAIN}: invalid PRIVATE_KEY_ALG - $PRIVATE_KEY_ALG" + config_errors=true ;; + esac + if [[ "$DUAL_RSA_ECDSA" == "true" ]] && [[ "$PRIVATE_KEY_ALG" == "rsa" ]]; then + info "${DOMAIN}: PRIVATE_KEY_ALG not set to an EC type and DUAL_RSA_ECDSA=\"true\"" + config_errors=true + fi + + # get all domains + if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then + alldomains=${SANS//,/ } + else + alldomains=$(echo "$DOMAIN,$SANS" | sed "s/,/ /g") + fi + if [[ -z "$alldomains" ]]; then + info "${DOMAIN}: no domains specified" + config_errors=true + fi + + if [[ $VALIDATE_VIA_DNS == "true" ]]; then # using dns-01 challenge + if [[ -z "$DNS_ADD_COMMAND" ]]; then + info "${DOMAIN}: DNS_ADD_COMMAND not defined (whilst VALIDATE_VIA_DNS=\"true\")" + config_errors=true + fi + if [[ -z "$DNS_DEL_COMMAND" ]]; then + info "${DOMAIN}: DNS_DEL_COMMAND not defined (whilst VALIDATE_VIA_DNS=\"true\")" + config_errors=true + fi + fi + + dn=0 + tmplist=$(mktemp) + for d in $alldomains; do # loop over domains (dn is domain number) + debug "checking domain $d" + if [[ "$(grep "^${d}$" "$tmplist")" = "$d" ]]; then + info "${DOMAIN}: $d appears to be duplicated in domain, SAN list" + config_errors=true + else + echo "$d" >> "$tmplist" + fi + + if [[ "$USE_SINGLE_ACL" == "true" ]]; then + DOMAIN_ACL="${ACL[0]}" + else + DOMAIN_ACL="${ACL[$dn]}" + fi + + if [[ $VALIDATE_VIA_DNS != "true" ]]; then # using http-01 challenge + if [[ -z "${DOMAIN_ACL}" ]]; then + info "${DOMAIN}: ACL location not specified for domain $d in $DOMAIN_DIR/getssl.cfg" + config_errors=true + fi + # check domain exist + if [[ "$DNS_CHECK_FUNC" == "drill" ]] || [[ "$DNS_CHECK_FUNC" == "dig" ]]; then + if [[ "$($DNS_CHECK_FUNC "${d}" SOA|grep -c "^${d}")" -ge 1 ]]; then + debug "found IP for ${d}" + else + info "${DOMAIN}: DNS lookup failed for ${d}" + config_errors=true + fi + elif [[ "$DNS_CHECK_FUNC" == "host" ]]; then + if [[ "$($DNS_CHECK_FUNC "${d}" |grep -c "^${d}")" -ge 1 ]]; then + debug "found IP for ${d}" + else + info "${DOMAIN}: DNS lookup failed for ${d}" + config_errors=true + fi + elif [[ "$(nslookup -query=AAAA "${d}"|grep -c "^${d}.*has AAAA address")" -ge 1 ]]; then + debug "found IPv6 record for ${d}" + elif [[ "$(nslookup "${d}"| grep -c ^Name)" -ge 1 ]]; then + debug "found IPv4 record for ${d}" + else + info "${DOMAIN}: DNS lookup failed for $d" + config_errors=true + fi + fi # end using http-01 challenge + ((dn++)) + done + + # tidy up + rm -f "$tmplist" + + if [[ "$config_errors" == "true" ]]; then + error_exit "${DOMAIN}: exiting due to config errors" + fi + debug "${DOMAIN}: check_config completed - all OK" +} + +check_getssl_upgrade() { # check if a more recent version of code is available available + TEMP_UPGRADE_FILE="$(mktemp)" + curl --silent "$CODE_LOCATION" --output "$TEMP_UPGRADE_FILE" + errcode=$? + if [[ $errcode -eq 60 ]]; then + error_exit "curl needs updating, your version does not support SNI (multiple SSL domains on a single IP)" + elif [[ $errcode -gt 0 ]]; then + error_exit "curl error : $errcode" + fi + latestversion=$(awk -F '"' '$1 == "VERSION=" {print $2}' "$TEMP_UPGRADE_FILE") + latestvdec=$(echo "$latestversion"| tr -d '.') + localvdec=$(echo "$VERSION"| tr -d '.' ) + debug "current code is version ${VERSION}" + debug "Most recent version is ${latestversion}" + # use a default of 0 for cases where the latest code has not been obtained. + if [[ "${latestvdec:-0}" -gt "$localvdec" ]]; then + if [[ ${_UPGRADE} -eq 1 ]]; then + install "$0" "${0}.v${VERSION}" + install -m 700 "$TEMP_UPGRADE_FILE" "$0" + if [[ ${_MUTE} -eq 0 ]]; then + echo "Updated getssl from v${VERSION} to v${latestversion}" + echo "these update notification can be turned off using the -Q option" + echo "" + echo "Updates are;" + awk "/\(${VERSION}\)$/ {s=1} s; /\(${latestversion}\)$/ {s=0}" "$TEMP_UPGRADE_FILE" | awk '{if(NR>1)print}' + echo "" + fi + if [[ -n "$_KEEP_VERSIONS" ]] && [[ "$_KEEP_VERSIONS" =~ ^[0-9]+$ ]]; then + # Obtain all locally stored old versions in getssl_versions + declare -a getssl_versions + shopt -s nullglob + for getssl_version in $0.v*; do + getssl_versions[${#getssl_versions[@]}]="$getssl_version" + done + shopt -u nullglob + # Explicitly sort the getssl_versions array to make sure + shopt -s -o noglob + IFS=$'\n' getssl_versions=($(sort <<< "${getssl_versions[*]}")) + shopt -u -o noglob + # Remove entries until given amount of old versions to keep is reached + while [[ ${#getssl_versions[@]} -gt $_KEEP_VERSIONS ]]; do + debug "removing old version ${getssl_versions[0]}" + rm "${getssl_versions[0]}" + getssl_versions=("${getssl_versions[@]:1}") + done + fi + eval "$ORIGCMD" + graceful_exit + else + info "" + info "A more recent version (v${latestversion}) of getssl is available, please update" + info "the easiest way is to use the -u or --upgrade flag" + info "" + fi + fi +} + +clean_up() { # Perform pre-exit housekeeping + umask "$ORIG_UMASK" + if [[ $VALIDATE_VIA_DNS == "true" ]]; then + # Tidy up DNS entries if things failed part way though. + shopt -s nullglob + for dnsfile in $TEMP_DIR/dns_verify/*; do + # shellcheck source=/dev/null + . "$dnsfile" + debug "attempting to clean up DNS entry for $d" + eval "$DNS_DEL_COMMAND" "$d" "$auth_key" + done + shopt -u nullglob + fi + if [[ ! -z "$DOMAIN_DIR" ]]; then + rm -rf "${TEMP_DIR:?}" + fi + if [[ ! -z "$TEMP_UPGRADE_FILE" ]] && [[ -f "$TEMP_UPGRADE_FILE" ]]; then + rm -f "$TEMP_UPGRADE_FILE" + fi +} + +copy_file_to_location() { # copies a file, using scp, sftp or ftp if required. + cert=$1 # descriptive name, just used for display + from=$2 # current file location + to=$3 # location to move file to. + IFS=\; read -r -a copy_locations <<<"$3" + for to in "${copy_locations[@]}"; do + info "copying $cert to $to" + debug "copying from $from to $to" + if [[ "${to:0:4}" == "ssh:" ]] ; then + debug "using scp scp -q $from ${to:4}" + if ! scp -q "$from" "${to:4}" >/dev/null 2>&1 ; then + error_exit "problem copying file to the server using scp. + scp $from ${to:4}" + fi + debug "userid $TOKEN_USER_ID" + if [[ "$cert" == "challenge token" ]] && [[ ! -z "$TOKEN_USER_ID" ]]; then + servername=$(echo "$to" | awk -F":" '{print $2}') + tofile=$(echo "$to" | awk -F":" '{print $3}') + debug "servername $servername" + debug "file $tofile" + # shellcheck disable=SC2029 + ssh "$servername" "chown $TOKEN_USER_ID $tofile" + fi + elif [[ "${to:0:4}" == "ftp:" ]] ; then + if [[ "$cert" != "challenge token" ]] ; then + error_exit "ftp is not a sercure method for copying certificates or keys" + fi + debug "using ftp to copy the file from $from" + ftpuser=$(echo "$to"| awk -F: '{print $2}') + ftppass=$(echo "$to"| awk -F: '{print $3}') + ftphost=$(echo "$to"| awk -F: '{print $4}') + ftplocn=$(echo "$to"| awk -F: '{print $5}') + ftpdirn=$(dirname "$ftplocn") + ftpfile=$(basename "$ftplocn") + fromdir=$(dirname "$from") + fromfile=$(basename "$from") + debug "ftp user=$ftpuser - pass=$ftppass - host=$ftphost dir=$ftpdirn file=$ftpfile" + debug "from dir=$fromdir file=$fromfile" + ftp -n <<- _EOF + open $ftphost + user $ftpuser $ftppass + cd $ftpdirn + lcd $fromdir + put $fromfile + _EOF + elif [[ "${to:0:5}" == "sftp:" ]] ; then + debug "using sftp to copy the file from $from" + ftpuser=$(echo "$to"| awk -F: '{print $2}') + ftppass=$(echo "$to"| awk -F: '{print $3}') + ftphost=$(echo "$to"| awk -F: '{print $4}') + ftplocn=$(echo "$to"| awk -F: '{print $5}') + ftpdirn=$(dirname "$ftplocn") + ftpfile=$(basename "$ftplocn") + fromdir=$(dirname "$from") + fromfile=$(basename "$from") + debug "sftp user=$ftpuser - pass=$ftppass - host=$ftphost dir=$ftpdirn file=$ftpfile" + debug "from dir=$fromdir file=$fromfile" + sshpass -p "$ftppass" sftp "$ftpuser@$ftphost" <<- _EOF + cd $ftpdirn + lcd $fromdir + put $fromfile + _EOF + else + if ! mkdir -p "$(dirname "$to")" ; then + error_exit "cannot create ACL directory $(basename "$to")" + fi + if [[ "$GETSSL_IGNORE_CP_PRESERVE" == "true" ]]; then + if ! cp "$from" "$to" ; then + error_exit "cannot copy $from to $to" + fi + else + if ! cp -p "$from" "$to" ; then + error_exit "cannot copy $from to $to" + fi + fi + if [[ "$cert" == "challenge token" ]] && [[ ! -z "$TOKEN_USER_ID" ]]; then + chown "$TOKEN_USER_ID" "$to" + fi + fi + debug "copied $from to $to" + done +} + +create_csr() { # create a csr using a given key (if it doesn't already exist) + csr_file=$1 + csr_key=$2 + # check if domain csr exists - if not then create it + if [[ -s "$csr_file" ]]; then + debug "domain csr exists at - $csr_file" + # check all domains in config are in csr + if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then + alldomains=$(echo "$SANS" | sed -e 's/ //g; s/,$//; y/,/\n/' | sort -u) + else + alldomains=$(echo "$DOMAIN,$SANS" | sed -e 's/ //g; s/,$//; y/,/\n/' | sort -u) + fi + domains_in_csr=$(openssl req -text -noout -in "$csr_file" \ + | sed -n -e 's/^ *Subject: .* CN=\([A-Za-z0-9.-]*\).*$/\1/p; /^ *DNS:.../ { s/ *DNS://g; y/,/\n/; p; }' \ + | sort -u) + for d in $alldomains; do + if [[ "$(echo "${domains_in_csr}"| grep "^${d}$")" != "${d}" ]]; then + info "existing csr at $csr_file does not contain ${d} - re-create-csr"\ + ".... $(echo "${domains_in_csr}"| grep "^${d}$")" + _RECREATE_CSR=1 + fi + done + # check all domains in csr are in config + if [[ "$alldomains" != "$domains_in_csr" ]]; then + info "existing csr at $csr_file does not have the same domains as the config - re-create-csr" + _RECREATE_CSR=1 + fi + fi + # end of ... check if domain csr exists - if not then create it + + # if CSR does not exist, or flag set to recreate, then create csr + if [[ ! -s "$csr_file" ]] || [[ "$_RECREATE_CSR" == "1" ]]; then + info "creating domain csr - $csr_file" + # create a temporary config file, for portability. + tmp_conf=$(mktemp) + cat "$SSLCONF" > "$tmp_conf" + printf "[SAN]\n%s" "$SANLIST" >> "$tmp_conf" + # add OCSP Must-Staple to the domain csr + # if openssl version >= 1.1.0 one can also use "tlsfeature = status_request" + if [[ "$OCSP_MUST_STAPLE" == "true" ]]; then + printf "\n1.3.6.1.5.5.7.1.24 = DER:30:03:02:01:05" >> "$tmp_conf" + fi + openssl req -new -sha256 -key "$csr_key" -subj "$CSR_SUBJECT" -reqexts SAN -config "$tmp_conf" > "$csr_file" + rm -f "$tmp_conf" + fi +} + +create_key() { # create a domain key (if it doesn't already exist) + key_type=$1 # domain key type + key_loc=$2 # domain key location + key_len=$3 # domain key length - for rsa keys. + # check if key exists, if not then create it. + if [[ -s "$key_loc" ]]; then + debug "domain key exists at $key_loc - skipping generation" + # ideally need to check validity of domain key + else + umask 077 + info "creating key - $key_loc" + case "$key_type" in + rsa) + openssl genrsa "$key_len" > "$key_loc";; + prime256v1|secp384r1|secp521r1) + openssl ecparam -genkey -name "$key_type" > "$key_loc";; + *) + error_exit "unknown private key algorithm type $key_loc";; + esac + umask "$ORIG_UMASK" + # remove csr on generation of new domain key + if [[ -e "${key_loc::-4}.csr" ]]; then + rm -f "${key_loc::-4}.csr" + fi + fi +} + +date_epoc() { # convert the date into epoch time + if [[ "$os" == "bsd" ]]; then + date -j -f "%b %d %T %Y %Z" "$1" +%s + elif [[ "$os" == "mac" ]]; then + date -j -f "%b %d %T %Y %Z" "$1" +%s + elif [[ "$os" == "busybox" ]]; then + de_ld=$(echo "$1" | awk '{print $1 $2 $3 $4}') + date -D "%b %d %T %Y" -d "$de_ld" +%s + else + date -d "$1" +%s + fi + +} + +date_fmt() { # format date from epoc time to YYYY-MM-DD + if [[ "$os" == "bsd" ]]; then #uses older style date function. + date -j -f "%s" "$1" +%F + elif [[ "$os" == "mac" ]]; then # MAC OSX uses older BSD style date. + date -j -f "%s" "$1" +%F + else + date -d "@$1" +%F + fi +} + +date_renew() { # calculates the renewal time in epoch + date_now_s=$( date +%s ) + echo "$((date_now_s + RENEW_ALLOW*24*60*60))" +} + +debug() { # write out debug info if the debug flag has been set + if [[ ${_USE_DEBUG} -eq 1 ]]; then + echo " " + echo "$@" + fi +} + +error_exit() { # give error message on error exit + echo -e "${PROGNAME}: ${1:-"Unknown Error"}" >&2 + clean_up + exit 1 +} + +get_auth_dns() { # get the authoritative dns server for a domain (sets primary_ns ) + gad_d="$1" # domain name + gad_s="$PUBLIC_DNS_SERVER" # start with PUBLIC_DNS_SERVER + + if [[ "$os" == "cygwin" ]]; then + all_auth_dns_servers=$(nslookup -type=soa "${d}" ${PUBLIC_DNS_SERVER} 2>/dev/null \ + | grep "primary name server" \ + | awk '{print $NF}') + if [[ -z "$all_auth_dns_servers" ]]; then + error_exit "couldn't find primary DNS server - please set AUTH_DNS_SERVER in config" + fi + primary_ns="$all_auth_dns_servers" + return + fi + + if [[ "$DNS_CHECK_FUNC" == "drill" ]] || [[ "$DNS_CHECK_FUNC" == "dig" ]]; then + if [[ -z "$gad_s" ]]; then #checking for CNAMEs + res=$($DNS_CHECK_FUNC CNAME "$gad_d"| grep "^$gad_d") + else + res=$($DNS_CHECK_FUNC CNAME "$gad_d" "@$gad_s"| grep "^$gad_d") + fi + if [[ ! -z "$res" ]]; then # domain is a CNAME so get main domain + gad_d=$(echo "$res"| awk '{print $5}' |sed 's/\.$//g') + fi + if [[ -z "$gad_s" ]]; then #checking for CNAMEs + res=$($DNS_CHECK_FUNC NS "$gad_d"| grep "^$gad_d") + else + res=$($DNS_CHECK_FUNC NS "$gad_d" "@$gad_s"| grep "^$gad_d") + fi + if [[ -z "$res" ]]; then + error_exit "couldn't find primary DNS server - please set AUTH_DNS_SERVER in config" + else + all_auth_dns_servers=$(echo "$res" | awk '$4 ~ "NS" {print $5}' | sed 's/\.$//g'|tr '\n' ' ') + fi + if [[ $CHECK_ALL_AUTH_DNS == "true" ]]; then + primary_ns="$all_auth_dns_servers" + else + primary_ns=$(echo "$all_auth_dns_servers" | awk '{print $1}') + fi + return + fi + + if [[ "$DNS_CHECK_FUNC" == "host" ]]; then + if [[ -z "$gad_s" ]]; then + res=$($DNS_CHECK_FUNC -t NS "$gad_d"| grep "name server") + else + res=$($DNS_CHECK_FUNC -t NS "$gad_d" "$gad_s"| grep "name server") + fi + if [[ -z "$res" ]]; then + error_exit "couldn't find primary DNS server - please set AUTH_DNS_SERVER in config" + else + all_auth_dns_servers=$(echo "$res" | awk '{print $4}' | sed 's/\.$//g'|tr '\n' ' ') + fi + if [[ $CHECK_ALL_AUTH_DNS == "true" ]]; then + primary_ns="$all_auth_dns_servers" + else + primary_ns=$(echo "$all_auth_dns_servers" | awk '{print $1}') + fi + return + fi + + res=$(nslookup -debug=1 -type=soa -type=ns "$gad_d" ${gad_s}) + + if [[ "$(echo "$res" | grep -c "Non-authoritative")" -gt 0 ]]; then + # this is a Non-authoritative server, need to check for an authoritative one. + gad_s=$(echo "$res" | awk '$2 ~ "nameserver" {print $4; exit }' |sed 's/\.$//g') + if [[ "$(echo "$res" | grep -c "an't find")" -gt 0 ]]; then + # if domain name doesn't exist, then find auth servers for next level up + gad_s=$(echo "$res" | awk '$1 ~ "origin" {print $3; exit }') + gad_d=$(echo "$res" | awk '$1 ~ "->" {print $2; exit}') + fi + fi + + if [[ -z "$gad_s" ]]; then + res=$(nslookup -debug=1 -type=soa -type=ns "$gad_d") + else + res=$(nslookup -debug=1 -type=soa -type=ns "$gad_d" "${gad_s}") + fi + + if [[ "$(echo "$res" | grep -c "canonical name")" -gt 0 ]]; then + gad_d=$(echo "$res" | awk ' $2 ~ "canonical" {print $5; exit }' |sed 's/\.$//g') + elif [[ "$(echo "$res" | grep -c "an't find")" -gt 0 ]]; then + gad_s=$(echo "$res" | awk ' $1 ~ "origin" {print $3; exit }') + gad_d=$(echo "$res"| awk '$1 ~ "->" {print $2; exit}') + fi + + all_auth_dns_servers=$(nslookup -type=soa -type=ns "$gad_d" "$gad_s" \ + | awk ' $2 ~ "nameserver" {print $4}' \ + | sed 's/\.$//g'| tr '\n' ' ') + if [[ $CHECK_ALL_AUTH_DNS == "true" ]]; then + primary_ns="$all_auth_dns_servers" + else + primary_ns=$(echo "$all_auth_dns_servers" | awk '{print $1}') + fi +} + +get_certificate() { # get certificate for csr, if all domains validated. + gc_csr=$1 # the csr file + gc_certfile=$2 # The filename for the certificate + gc_cafile=$3 # The filename for the CA certificate + + der=$(openssl req -in "$gc_csr" -outform DER | urlbase64) + debug "der $der" + send_signed_request "$URL_new_cert" "{\"resource\": \"new-cert\", \"csr\": \"$der\"}" "needbase64" + + # convert certificate information into correct format and save to file. + CertData=$(awk ' $1 ~ "^Location" {print $2}' "$CURL_HEADER" |tr -d '\r') + debug "certdata location = $CertData" + if [[ "$CertData" ]] ; then + echo -----BEGIN CERTIFICATE----- > "$gc_certfile" + curl --silent "$CertData" | openssl base64 -e >> "$gc_certfile" + echo -----END CERTIFICATE----- >> "$gc_certfile" + info "Certificate saved in $CERT_FILE" + fi + + # If certificate wasn't a valid certificate, error exit. + if [[ -z "$CertData" ]] ; then + response2=$(echo "$response" | fold -w64 |openssl base64 -d) + debug "response was $response" + error_exit "Sign failed: $(echo "$response2" | grep "detail")" + fi + + # get a copy of the CA certificate. + IssuerData=$(grep -i '^Link' "$CURL_HEADER" \ + | cut -d " " -f 2\ + | cut -d ';' -f 1 \ + | sed 's///g') + if [[ "$IssuerData" ]] ; then + echo -----BEGIN CERTIFICATE----- > "$gc_cafile" + curl --silent "$IssuerData" | openssl base64 -e >> "$gc_cafile" + echo -----END CERTIFICATE----- >> "$gc_cafile" + info "The intermediate CA cert is in $gc_cafile" + fi +} + +get_cr() { # get curl response + url="$1" + debug url "$url" + response=$(curl --silent "$url") + ret=$? + debug response "$response" + code=$(json_get "$response" status) + debug code "$code" + debug "get_cr return code $ret" + return $ret +} + +get_os() { # function to get the current Operating System + uname_res=$(uname -s) + if [[ $(date -h 2>&1 | grep -ic busybox) -gt 0 ]]; then + os="busybox" + elif [[ ${uname_res} == "Linux" ]]; then + os="linux" + elif [[ ${uname_res} == "FreeBSD" ]]; then + os="bsd" + elif [[ ${uname_res} == "Darwin" ]]; then + os="mac" + elif [[ ${uname_res:0:6} == "CYGWIN" ]]; then + os="cygwin" + elif [[ ${uname_res:0:5} == "MINGW" ]]; then + os="mingw" + else + os="unknown" + fi + debug "detected os type = $os" +} + +get_signing_params() { # get signing parameters from key + skey=$1 + if openssl rsa -in "${skey}" -noout 2>/dev/null ; then # RSA key + pub_exp64=$(openssl rsa -in "${skey}" -noout -text \ + | grep publicExponent \ + | grep -oE "0x[a-f0-9]+" \ + | cut -d'x' -f2 \ + | hex2bin \ + | urlbase64) + pub_mod64=$(openssl rsa -in "${skey}" -noout -modulus \ + | cut -d'=' -f2 \ + | hex2bin \ + | urlbase64) + + jwk='{"e":"'"${pub_exp64}"'","kty":"RSA","n":"'"${pub_mod64}"'"}' + jwkalg="RS256" + signalg="sha256" + elif openssl ec -in "${skey}" -noout 2>/dev/null ; then # Elliptic curve key. + crv="$(openssl ec -in "$skey" -noout -text 2>/dev/null | awk '$2 ~ "CURVE:" {print $3}')" + if [[ -z "$crv" ]]; then + gsp_keytype="$(openssl ec -in "$skey" -noout -text 2>/dev/null \ + | grep "^ASN1 OID:" \ + | awk '{print $3}')" + case "$gsp_keytype" in + prime256v1) crv="P-256" ;; + secp384r1) crv="P-384" ;; + secp521r1) crv="P-521" ;; + *) error_exit "invalid curve algorithm type $gsp_keytype";; + esac + fi + case "$crv" in + P-256) jwkalg="ES256" ; signalg="sha256" ;; + P-384) jwkalg="ES384" ; signalg="sha384" ;; + P-521) jwkalg="ES512" ; signalg="sha512" ;; + *) error_exit "invalid curve algorithm type $crv";; + esac + pubtext="$(openssl ec -in "$skey" -noout -text 2>/dev/null \ + | awk '/^pub:/{p=1;next}/^ASN1 OID:/{p=0}p' \ + | tr -d ": \n\r")" + mid=$(( (${#pubtext} -2) / 2 + 2 )) + debug "pubtext = $pubtext" + x64=$(echo "$pubtext" | cut -b 3-$mid | hex2bin | urlbase64) + y64=$(echo "$pubtext" | cut -b $((mid+1))-${#pubtext} | hex2bin | urlbase64) + jwk='{"crv":"'"$crv"'","kty":"EC","x":"'"$x64"'","y":"'"$y64"'"}' + debug "jwk $jwk" + else + error_exit "Invalid key file" + fi + thumbprint="$(printf "%s" "$jwk" | openssl dgst -sha256 -binary | urlbase64)" + debug "jwk alg = $jwkalg" + debug "jwk = $jwk" + debug "thumbprint $thumbprint" +} + +graceful_exit() { # normal exit function. + clean_up + exit +} + +help_message() { # print out the help message + cat <<- _EOF_ + $PROGNAME ver. $VERSION + Obtain SSL certificates from the letsencrypt.org ACME server + + $(usage) + + Options: + -a, --all Check all certificates + -d, --debug Outputs debug information + -c, --create Create default config files + -f, --force Force renewal of cert (overrides expiry checks) + -h, --help Display this help message and exit + -q, --quiet Quiet mode (only outputs on error, success of new cert, or getssl was upgraded) + -Q, --mute Like -q, but mutes notification about successful upgrade + -r, --revoke "cert" "key" [CA_server] Revoke a certificate (the cert and key are required) + -u, --upgrade Upgrade getssl if a more recent version is available + -k, --keep "#" Maximum amount of old getssl versions to keep when upgrading + -U, --nocheck Do not check if a more recent version is available + -w working_dir "Working directory" + + _EOF_ +} + +hex2bin() { # Remove spaces, add leading zero, escape as hex string ensuring no trailing new line char +# printf -- "$(cat | os_esed -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')" + echo -e -n "$(cat | os_esed -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')" +} + +info() { # write out info as long as the quiet flag has not been set. + if [[ ${_QUIET} -eq 0 ]]; then + echo "$@" + fi +} + +json_get() { # get the value corresponding to $2 in the JSON passed as $1. + # remove newlines, so it's a single chunk of JSON + json_data=$( echo "$1" | tr '\n' ' ') + # if $3 is defined, this is the section which the item is in. + if [[ ! -z "$3" ]]; then + jg_section=$(echo "$json_data" | awk -F"[}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${3}"'\"/){print $i}}}') + if [[ "$2" == "uri" ]]; then + jg_subsect=$(echo "$jg_section" | awk -F"[,]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i)}}}') + jg_result=$(echo "$jg_subsect" | awk -F'"' '{print $4}') + else + jg_result=$(echo "$jg_section" | awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i+1)}}}') + fi + else + jg_result=$(echo "$json_data" |awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i+1)}}}') + fi + # check number of quotes + jg_q=${jg_result//[^\"]/} + # if 2 quotes, assume it's a quoted variable and just return the data within the quotes. + if [[ ${#jg_q} -eq 2 ]]; then + echo "$jg_result" | awk -F'"' '{print $2}' + else + echo "$jg_result" + fi +} + +os_esed() { # Use different sed version for different os types (extended regex) + if [[ "$os" == "bsd" ]]; then # BSD requires -E flag for extended regex + sed -E "${@}" + elif [[ "$os" == "mac" ]]; then # MAC uses older BSD style sed. + sed -E "${@}" + else + sed -r "${@}" + fi +} + +purge_archive() { # purge archive of old, invalid, certificates + arcdir="$1/archive" + debug "purging archives in ${arcdir}/" + for padir in $arcdir/????_??_??_??_??; do + # check each directory + if [[ -d "$padir" ]]; then + tstamp=$(basename "$padir"| awk -F"_" '{print $1"-"$2"-"$3" "$4":"$5}') + if [[ "$os" == "bsd" ]]; then + direpoc=$(date -j -f "%F %H:%M" "$tstamp" +%s) + elif [[ "$os" == "mac" ]]; then + direpoc=$(date -j -f "%F %H:%M" "$tstamp" +%s) + else + direpoc=$(date -d "$tstamp" +%s) + fi + current_epoc=$(date "+%s") + # as certs currently valid for 90 days, purge anything older than 100 + purgedate=$((current_epoc - 60*60*24*100)) + if [[ "$direpoc" -lt "$purgedate" ]]; then + echo "purge $padir" + rm -rf "${padir:?}" + fi + fi + done +} + +reload_service() { # Runs a command to reload services ( via ssh if needed) + if [[ ! -z "$RELOAD_CMD" ]]; then + info "reloading SSL services" + if [[ "${RELOAD_CMD:0:4}" == "ssh:" ]] ; then + sshhost=$(echo "$RELOAD_CMD"| awk -F: '{print $2}') + command=${RELOAD_CMD:(( ${#sshhost} + 5))} + debug "running following command to reload cert" + debug "ssh $sshhost ${command}" + # shellcheck disable=SC2029 + ssh "$sshhost" "${command}" 1>/dev/null 2>&1 + # allow 2 seconds for services to restart + sleep 2 + else + debug "running reload command $RELOAD_CMD" + if ! eval "$RELOAD_CMD" ; then + error_exit "error running $RELOAD_CMD" + fi + fi + fi +} + +revoke_certificate() { # revoke a certificate + debug "revoking cert $REVOKE_CERT" + debug "using key $REVOKE_KEY" + ACCOUNT_KEY="$REVOKE_KEY" + # need to set the revoke key as "account_key" since it's used in send_signed_request. + get_signing_params "$REVOKE_KEY" + TEMP_DIR=$(mktemp -d) + debug "revoking from $CA" + rcertdata=$(openssl x509 -in "$REVOKE_CERT" -inform PEM -outform DER | urlbase64) + send_signed_request "$URL_revoke" "{\"resource\": \"revoke-cert\", \"certificate\": \"$rcertdata\"}" + if [[ $code -eq "200" ]]; then + info "certificate revoked" + else + error_exit "Revocation failed: $(echo "$response" | grep "detail")" + fi +} + +requires() { # check if required function is available + if [[ "$#" -gt 1 ]]; then # if more than 1 value, check list + for i in "$@"; do + if [[ "$i" == "${!#}" ]]; then # if on last variable then exit as not found + error_exit "this script requires one of: ${*:1:$(($#-1))}" + fi + res=$(which "$i" 2>/dev/null) + debug "checking for $i ... $res" + if [[ ! -z "$res" ]]; then # if function found, then set variable to function and return + debug "function $i found at $res - setting ${!#} to $i" + eval "${!#}=\$i" + return + fi + done + else # only one value, so check it. + result=$(which "$1" 2>/dev/null) + debug "checking for required $1 ... $result" + if [[ -z "$result" ]]; then + error_exit "This script requires $1 installed" + fi + fi +} + +set_server_type() { # uses SERVER_TYPE to set REMOTE_PORT and REMOTE_EXTRA + if [[ ${SERVER_TYPE} == "https" ]] || [[ ${SERVER_TYPE} == "webserver" ]]; then + REMOTE_PORT=443 + elif [[ ${SERVER_TYPE} == "ftp" ]]; then + REMOTE_PORT=21 + REMOTE_EXTRA="-starttls ftp" + elif [[ ${SERVER_TYPE} == "ftpi" ]]; then + REMOTE_PORT=990 + elif [[ ${SERVER_TYPE} == "imap" ]]; then + REMOTE_PORT=143 + REMOTE_EXTRA="-starttls imap" + elif [[ ${SERVER_TYPE} == "imaps" ]]; then + REMOTE_PORT=993 + elif [[ ${SERVER_TYPE} == "pop3" ]]; then + REMOTE_PORT=110 + REMOTE_EXTRA="-starttls pop3" + elif [[ ${SERVER_TYPE} == "pop3s" ]]; then + REMOTE_PORT=995 + elif [[ ${SERVER_TYPE} == "smtp" ]]; then + REMOTE_PORT=25 + REMOTE_EXTRA="-starttls smtp" + elif [[ ${SERVER_TYPE} == "smtps_deprecated" ]]; then + REMOTE_PORT=465 + elif [[ ${SERVER_TYPE} == "smtps" ]] || [[ ${SERVER_TYPE} == "smtp_submission" ]]; then + REMOTE_PORT=587 + REMOTE_EXTRA="-starttls smtp" + elif [[ ${SERVER_TYPE} == "xmpp" ]]; then + REMOTE_PORT=5222 + REMOTE_EXTRA="-starttls xmpp" + elif [[ ${SERVER_TYPE} == "xmpps" ]]; then + REMOTE_PORT=5269 + elif [[ ${SERVER_TYPE} == "ldaps" ]]; then + REMOTE_PORT=636 + elif [[ ${SERVER_TYPE} =~ ^[0-9]+$ ]]; then + REMOTE_PORT=${SERVER_TYPE} + else + info "${DOMAIN}: unknown server type \"$SERVER_TYPE\" in SERVER_TYPE" + config_errors=true + fi +} + +send_signed_request() { # Sends a request to the ACME server, signed with your private key. + url=$1 + payload=$2 + needbase64=$3 + + debug url "$url" + debug payload "$payload" + + CURL_HEADER="$TEMP_DIR/curl.header" + dp="$TEMP_DIR/curl.dump" + CURL="curl --silent --dump-header $CURL_HEADER " + if [[ ${_USE_DEBUG} -eq 1 ]]; then + CURL="$CURL --trace-ascii $dp " + fi + + # convert payload to url base 64 + payload64="$(printf '%s' "${payload}" | urlbase64)" + debug payload64 "$payload64" + + # get nonce from ACME server + nonceurl="$CA/directory" + nonce=$($CURL -I $nonceurl | grep "^Replay-Nonce:" | awk '{print $2}' | tr -d '\r\n ') + + debug nonce "$nonce" + + # Build header with just our public key and algorithm information + header='{"alg": "'"$jwkalg"'", "jwk": '"$jwk"'}' + + # Build another header which also contains the previously received nonce and encode it as urlbase64 + protected='{"alg": "'"$jwkalg"'", "jwk": '"$jwk"', "nonce": "'"${nonce}"'", "url": "'"${url}"'"}' + protected64="$(printf '%s' "${protected}" | urlbase64)" + debug protected "$protected" + + # Sign header with nonce and our payload with our private key and encode signature as urlbase64 + sign_string "$(printf '%s' "${protected64}.${payload64}")" "${ACCOUNT_KEY}" "$signalg" + + # Send header + extended header + payload + signature to the acme-server + body="{\"header\": ${header}," + body="${body}\"protected\": \"${protected64}\"," + body="${body}\"payload\": \"${payload64}\"," + body="${body}\"signature\": \"${signed64}\"}" + debug "header, payload and signature = $body" + + code="500" + loop_limit=5 + while [[ "$code" -eq 500 ]]; do + if [[ "$needbase64" ]] ; then + response=$($CURL -X POST --data "$body" "$url" | urlbase64) + else + response=$($CURL -X POST --data "$body" "$url") + fi + + responseHeaders=$(cat "$CURL_HEADER") + debug responseHeaders "$responseHeaders" + debug response "$response" + code=$(awk ' $1 ~ "^HTTP" {print $2}' "$CURL_HEADER" | tail -1) + debug code "$code" + response_status=$(json_get "$response" status \ + | head -1| awk -F'"' '{print $2}') + debug "response status = $response_status" + + if [[ "$code" -eq 500 ]]; then + info "error on acme server - trying again ...." + sleep 2 + loop_limit=$((loop_limit - 1)) + if [[ $loop_limit -lt 1 ]]; then + error_exit "500 error from ACME server: $response" + fi + fi + done +} + +sign_string() { # sign a string with a given key and algorithm and return urlbase64 + # sets the result in variable signed64 + str=$1 + key=$2 + signalg=$3 + + if openssl rsa -in "${skey}" -noout 2>/dev/null ; then # RSA key + signed64="$(printf '%s' "${str}" | openssl dgst -"$signalg" -sign "$key" | urlbase64)" + elif openssl ec -in "${skey}" -noout 2>/dev/null ; then # Elliptic curve key. + signed=$(printf '%s' "${str}" | openssl dgst -"$signalg" -sign "$key" -hex | awk '{print $2}') + debug "EC signature $signed" + if [[ "${signed:4:4}" == "0220" ]]; then #sha256 + R=$(echo "$signed" | cut -c 9-72) + part2=$(echo "$signed" | cut -c 73-) + elif [[ "${signed:4:4}" == "0221" ]]; then #sha256 + R=$(echo "$signed" | cut -c 11-74) + part2=$(echo "$signed" | cut -c 75-) + elif [[ "${signed:4:4}" == "0230" ]]; then #sha384 + R=$(echo "$signed" | cut -c 9-104) + part2=$(echo "$signed" | cut -c 105-) + elif [[ "${signed:4:4}" == "0231" ]]; then #sha384 + R=$(echo "$signed" | cut -c 11-106) + part2=$(echo "$signed" | cut -c 107-) + elif [[ "${signed:6:4}" == "0241" ]]; then #sha512 + R=$(echo "$signed" | cut -c 11-140) + part2=$(echo "$signed" | cut -c 141-) + elif [[ "${signed:6:4}" == "0242" ]]; then #sha512 + R=$(echo "$signed" | cut -c 11-142) + part2=$(echo "$signed" | cut -c 143-) + else + error_exit "error in EC signing couldn't get R from $signed" + fi + debug "R $R" + + if [[ "${part2:0:4}" == "0220" ]]; then #sha256 + S=$(echo "$part2" | cut -c 5-68) + elif [[ "${part2:0:4}" == "0221" ]]; then #sha256 + S=$(echo "$part2" | cut -c 7-70) + elif [[ "${part2:0:4}" == "0230" ]]; then #sha384 + S=$(echo "$part2" | cut -c 5-100) + elif [[ "${part2:0:4}" == "0231" ]]; then #sha384 + S=$(echo "$part2" | cut -c 7-102) + elif [[ "${part2:0:4}" == "0241" ]]; then #sha512 + S=$(echo "$part2" | cut -c 5-136) + elif [[ "${part2:0:4}" == "0242" ]]; then #sha512 + S=$(echo "$part2" | cut -c 5-136) + else + error_exit "error in EC signing couldn't get S from $signed" + fi + + debug "S $S" + signed64=$(printf '%s' "${R}${S}" | hex2bin | urlbase64 ) + debug "encoded RS $signed64" + fi +} + +signal_exit() { # Handle trapped signals + case $1 in + INT) + error_exit "Program interrupted by user" ;; + TERM) + echo -e "\n$PROGNAME: Program terminated" >&2 + graceful_exit ;; + *) + error_exit "$PROGNAME: Terminating on unknown signal" ;; + esac +} + +urlbase64() { # urlbase64: base64 encoded string with '+' replaced with '-' and '/' replaced with '_' + openssl base64 -e | tr -d '\n\r' | os_esed -e 's:=*$::g' -e 'y:+/:-_:' +} + +usage() { # echos out the program usage + echo "Usage: $PROGNAME [-h|--help] [-d|--debug] [-c|--create] [-f|--force] [-a|--all] [-q|--quiet]"\ + "[-Q|--mute] [-u|--upgrade] [-k|--keep #] [-U|--nocheck] [-r|--revoke cert key] [-w working_dir] domain" +} + +write_domain_template() { # write out a template file for a domain. + cat > "$1" <<- _EOF_domain_ + # Uncomment and modify any variables you need + # see https://github.com/srvrco/getssl/wiki/Config-variables for details + # see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs + # + # The staging server is best for testing + #CA="https://acme-staging.api.letsencrypt.org" + # This server issues full certificates, however has rate limits + #CA="https://acme-v01.api.letsencrypt.org" + + #PRIVATE_KEY_ALG="rsa" + + # Additional domains - this could be multiple domains / subdomains in a comma separated list + # Note: this is Additional domains - so should not include the primary domain. + SANS="${EX_SANS}" + + # Acme Challenge Location. The first line for the domain, the following ones for each additional domain. + # If these start with ssh: then the next variable is assumed to be the hostname and the rest the location. + # An ssh key will be needed to provide you with access to the remote server. + # Optionally, you can specify a different userid for ssh/scp to use on the remote server before the @ sign. + # If left blank, the username on the local server will be used to authenticate against the remote server. + # If these start with ftp: then the next variables are ftpuserid:ftppassword:servername:ACL_location + # These should be of the form "/path/to/your/website/folder/.well-known/acme-challenge" + # where "/path/to/your/website/folder/" is the path, on your web server, to the web root for your domain. + #ACL=('/var/www/${DOMAIN}/web/.well-known/acme-challenge' + # 'ssh:server5:/var/www/${DOMAIN}/web/.well-known/acme-challenge' + # 'ssh:sshuserid@server5:/var/www/${DOMAIN}/web/.well-known/acme-challenge' + # 'ftp:ftpuserid:ftppassword:${DOMAIN}:/web/.well-known/acme-challenge') + + #Set USE_SINGLE_ACL="true" to use a single ACL for all checks + #USE_SINGLE_ACL="false" + + # Location for all your certs, these can either be on the server (full path name) + # or using ssh /sftp as for the ACL + #DOMAIN_CERT_LOCATION="/etc/ssl/${DOMAIN}.crt" + #DOMAIN_KEY_LOCATION="/etc/ssl/${DOMAIN}.key" + #CA_CERT_LOCATION="/etc/ssl/chain.crt" + #DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert + #DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + + # The command needed to reload apache / nginx or whatever you use + #RELOAD_CMD="" + + # Define the server type. This can be https, ftp, ftpi, imap, imaps, pop3, pop3s, smtp, + # smtps_deprecated, smtps, smtp_submission, xmpp, xmpps, ldaps or a port number which + # will be checked for certificate expiry and also will be checked after + # an update to confirm correct certificate is running (if CHECK_REMOTE) is set to true + #SERVER_TYPE="https" + #CHECK_REMOTE="true" + _EOF_domain_ +} + +write_getssl_template() { # write out the main template file + cat > "$1" <<- _EOF_getssl_ + # Uncomment and modify any variables you need + # see https://github.com/srvrco/getssl/wiki/Config-variables for details + # + # The staging server is best for testing (hence set as default) + CA="https://acme-staging.api.letsencrypt.org" + # This server issues full certificates, however has rate limits + #CA="https://acme-v01.api.letsencrypt.org" + + #AGREEMENT="$AGREEMENT" + + # Set an email address associated with your account - generally set at account level rather than domain. + #ACCOUNT_EMAIL="me@example.com" + ACCOUNT_KEY_LENGTH=4096 + ACCOUNT_KEY="$WORKING_DIR/account.key" + PRIVATE_KEY_ALG="rsa" + #REUSE_PRIVATE_KEY="true" + + # The command needed to reload apache / nginx or whatever you use + #RELOAD_CMD="" + # The time period within which you want to allow renewal of a certificate + # this prevents hitting some of the rate limits. + RENEW_ALLOW="30" + + # Define the server type. This can be https, ftp, ftpi, imap, imaps, pop3, pop3s, smtp, + # smtps_deprecated, smtps, smtp_submission, xmpp, xmpps, ldaps or a port number which + # will be checked for certificate expiry and also will be checked after + # an update to confirm correct certificate is running (if CHECK_REMOTE) is set to true + SERVER_TYPE="https" + CHECK_REMOTE="true" + + # Use the following 3 variables if you want to validate via DNS + #VALIDATE_VIA_DNS="true" + #DNS_ADD_COMMAND= + #DNS_DEL_COMMAND= + _EOF_getssl_ +} + +write_openssl_conf() { # write out a minimal openssl conf + cat > "$1" <<- _EOF_openssl_conf_ + # minimal openssl.cnf file + distinguished_name = req_distinguished_name + [ req_distinguished_name ] + [v3_req] + [v3_ca] + _EOF_openssl_conf_ +} + +# Trap signals +trap "signal_exit TERM" TERM HUP +trap "signal_exit INT" INT + +# Parse command-line +while [[ -n ${1+defined} ]]; do + case $1 in + -h | --help) + help_message; graceful_exit ;; + -d | --debug) + _USE_DEBUG=1 ;; + -c | --create) + _CREATE_CONFIG=1 ;; + -f | --force) + _FORCE_RENEW=1 ;; + -a | --all) + _CHECK_ALL=1 ;; + -k | --keep) + shift; _KEEP_VERSIONS="$1";; + -q | --quiet) + _QUIET=1 ;; + -Q | --mute) + _QUIET=1 + _MUTE=1 ;; + -r | --revoke) + _REVOKE=1 + shift + REVOKE_CERT="$1" + shift + REVOKE_KEY="$1" + shift + REVOKE_CA="$1" ;; + -u | --upgrade) + _UPGRADE=1 ;; + -U | --nocheck) + _UPGRADE_CHECK=0 ;; + -w) + shift; WORKING_DIR="$1" ;; + -* | --*) + usage + error_exit "Unknown option $1" ;; + *) + if [[ ! -z $DOMAIN ]]; then + error_exit "invalid command line $DOMAIN - it appears to contain more than one domain" + fi + DOMAIN="$1" + if [[ -z $DOMAIN ]]; then + error_exit "invalid command line - it appears to contain a null variable" + fi ;; + esac + shift +done + +# Main logic +############ + +# Get the current OS, so the correct functions can be used for that OS. (sets the variable os) +get_os + +# check if "recent" version of bash. +#if [[ "${BASH_VERSINFO[0]}${BASH_VERSINFO[1]}" -lt 42 ]]; then +# info "this script is designed for bash v4.2 or later - earlier version may give errors" +#fi + +#check if required applications are included + +requires which +requires openssl +requires curl +requires nslookup drill dig host DNS_CHECK_FUNC +requires awk +requires tr +requires date +requires grep +requires sed +requires sort +requires mktemp + +# Check if upgrades are available (unless they have specified -U to ignore Upgrade checks) +if [[ $_UPGRADE_CHECK -eq 1 ]]; then + check_getssl_upgrade +fi + +# Revoke a certificate if requested +if [[ $_REVOKE -eq 1 ]]; then + if [[ -z $REVOKE_CA ]]; then + CA=$DEFAULT_REVOKE_CA + elif [[ "$REVOKE_CA" == "-d" ]]; then + _USE_DEBUG=1 + CA=$DEFAULT_REVOKE_CA + else + CA=$REVOKE_CA + fi + URL_revoke=$(curl "${CA}/directory" 2>/dev/null | grep "revoke-cert" | awk -F'"' '{print $4}') + revoke_certificate + graceful_exit +fi + +# get latest agreement from CA (as default) +AGREEMENT=$(curl -I "${CA}/terms" 2>/dev/null | awk '$1 ~ "Location:" {print $2}'|tr -d '\r') + +# if nothing in command line, print help and exit. +if [[ -z "$DOMAIN" ]] && [[ ${_CHECK_ALL} -ne 1 ]]; then + help_message + graceful_exit +fi + +# if the "working directory" doesn't exist, then create it. +if [[ ! -d "$WORKING_DIR" ]]; then + debug "Making working directory - $WORKING_DIR" + mkdir -p "$WORKING_DIR" +fi + +# read any variables from config in working directory +if [[ -s "$WORKING_DIR/getssl.cfg" ]]; then + debug "reading config from $WORKING_DIR/getssl.cfg" + # shellcheck source=/dev/null + . "$WORKING_DIR/getssl.cfg" +fi + +# Define defaults for variables not set in the main config. +ACCOUNT_KEY="${ACCOUNT_KEY:=$WORKING_DIR/account.key}" +DOMAIN_STORAGE="${DOMAIN_STORAGE:=$WORKING_DIR}" +DOMAIN_DIR="$DOMAIN_STORAGE/$DOMAIN" +CERT_FILE="$DOMAIN_DIR/${DOMAIN}.crt" +CA_CERT="$DOMAIN_DIR/chain.crt" +TEMP_DIR="$DOMAIN_DIR/tmp" +if [[ "$os" == "mingw" ]]; then + CSR_SUBJECT="//" +fi + +# Set the OPENSSL_CONF environment variable so openssl knows which config to use +export OPENSSL_CONF=$SSLCONF + +# if "-a" option then check other parameters and create run for each domain. +if [[ ${_CHECK_ALL} -eq 1 ]]; then + info "Check all certificates" + + if [[ ${_CREATE_CONFIG} -eq 1 ]]; then + error_exit "cannot combine -c|--create with -a|--all" + fi + + if [[ ${_FORCE_RENEW} -eq 1 ]]; then + error_exit "cannot combine -f|--force with -a|--all because of rate limits" + fi + + if [[ ! -d "$DOMAIN_STORAGE" ]]; then + error_exit "DOMAIN_STORAGE not found - $DOMAIN_STORAGE" + fi + + for dir in ${DOMAIN_STORAGE}/*; do + if [[ -d "$dir" ]]; then + debug "Checking $dir" + cmd="$0 -U" # No update checks when calling recursively + if [[ ${_USE_DEBUG} -eq 1 ]]; then + cmd="$cmd -d" + fi + if [[ ${_QUIET} -eq 1 ]]; then + cmd="$cmd -q" + fi + # check if $dir looks like a domain name (contains a period) + if [[ $(basename "$dir") == *.* ]]; then + cmd="$cmd -w $WORKING_DIR $(basename "$dir")" + debug "CMD: $cmd" + eval "$cmd" + fi + fi + done + + graceful_exit +fi +# end of "-a" option (looping through all domains) + +# if "-c|--create" option used, then create config files. +if [[ ${_CREATE_CONFIG} -eq 1 ]]; then + # If main config file does not exists then create it. + if [[ ! -s "$WORKING_DIR/getssl.cfg" ]]; then + info "creating main config file $WORKING_DIR/getssl.cfg" + if [[ ! -s "$SSLCONF" ]]; then + SSLCONF="$WORKING_DIR/openssl.cnf" + write_openssl_conf "$SSLCONF" + fi + write_getssl_template "$WORKING_DIR/getssl.cfg" + fi + # If domain and domain config don't exist then create them. + if [[ ! -d "$DOMAIN_DIR" ]]; then + info "Making domain directory - $DOMAIN_DIR" + mkdir -p "$DOMAIN_DIR" + fi + if [[ -s "$DOMAIN_DIR/getssl.cfg" ]]; then + info "domain config already exists $DOMAIN_DIR/getssl.cfg" + else + info "creating domain config file in $DOMAIN_DIR/getssl.cfg" + # if domain has an existing cert, copy from domain and use to create defaults. + EX_CERT=$(echo \ + | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:443" 2>/dev/null \ + | openssl x509 2>/dev/null) + EX_SANS="www.${DOMAIN}" + if [[ ! -z "${EX_CERT}" ]]; then + EX_SANS=$(echo "$EX_CERT" \ + | openssl x509 -noout -text 2>/dev/null| grep "Subject Alternative Name" -A2 \ + | grep -Eo "DNS:[a-zA-Z 0-9.-]*" | sed "s@DNS:$DOMAIN@@g" | grep -v '^$' | cut -c 5-) + EX_SANS=${EX_SANS//$'\n'/','} + fi + write_domain_template "$DOMAIN_DIR/getssl.cfg" + fi + TEMP_DIR="$DOMAIN_DIR/tmp" + # end of "-c|--create" option, so exit + graceful_exit +fi +# end of "-c|--create" option to create config file. + +# if domain directory doesn't exist, then create it. +if [[ ! -d "$DOMAIN_DIR" ]]; then + debug "Making working directory - $DOMAIN_DIR" + mkdir -p "$DOMAIN_DIR" +fi + +# define a temporary directory, and if it doesn't exist, create it. +TEMP_DIR="$DOMAIN_DIR/tmp" +if [[ ! -d "${TEMP_DIR}" ]]; then + debug "Making temp directory - ${TEMP_DIR}" + mkdir -p "${TEMP_DIR}" +fi + +# read any variables from config in domain directory +if [[ -s "$DOMAIN_DIR/getssl.cfg" ]]; then + debug "reading config from $DOMAIN_DIR/getssl.cfg" + # shellcheck source=/dev/null + . "$DOMAIN_DIR/getssl.cfg" +fi + +# from SERVER_TYPE set REMOTE_PORT and REMOTE_EXTRA +set_server_type + +# check config for typical errors. +check_config + +if [[ -e "$DOMAIN_DIR/FORCE_RENEWAL" ]]; then + rm -f "$DOMAIN_DIR/FORCE_RENEWAL" || error_exit "problem deleting file $DOMAIN_DIR/FORCE_RENEWAL" + _FORCE_RENEW=1 + info "${DOMAIN}: forcing renewal (due to FORCE_RENEWAL file)" +fi + +# Obtain CA resource locations +ca_all_loc=$(curl "${CA}/directory" 2>/dev/null) +URL_new_reg=$(echo "$ca_all_loc" | grep "new-reg" | awk -F'"' '{print $4}') +URL_new_authz=$(echo "$ca_all_loc" | grep "new-authz" | awk -F'"' '{print $4}') +URL_new_cert=$(echo "$ca_all_loc" | grep "new-cert" | awk -F'"' '{print $4}') + +# if check_remote is true then connect and obtain the current certificate (if not forcing renewal) +if [[ "${CHECK_REMOTE}" == "true" ]] && [[ $_FORCE_RENEW -eq 0 ]]; then + debug "getting certificate for $DOMAIN from remote server" + # shellcheck disable=SC2086 + EX_CERT=$(echo \ + | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} 2>/dev/null \ + | openssl x509 2>/dev/null) + if [[ ! -z "$EX_CERT" ]]; then # if obtained a cert + if [[ -s "$CERT_FILE" ]]; then # if local exists + CERT_LOCAL=$(openssl x509 -noout -fingerprint < "$CERT_FILE" 2>/dev/null) + else # since local doesn't exist leave empty so that the domain validation will happen + CERT_LOCAL="" + fi + CERT_REMOTE=$(echo "$EX_CERT" | openssl x509 -noout -fingerprint 2>/dev/null) + if [[ "$CERT_LOCAL" == "$CERT_REMOTE" ]]; then + debug "certificate on server is same as the local cert" + else + # check if the certificate is for the right domain + EX_CERT_DOMAIN=$(echo "$EX_CERT" | openssl x509 -text \ + | sed -n -e 's/^ *Subject: .* CN=\([A-Za-z0-9.-]*\).*$/\1/p; /^ *DNS:.../ { s/ *DNS://g; y/,/\n/; p; }' \ + | sort -u | grep "^$DOMAIN\$") + if [[ "$EX_CERT_DOMAIN" == "$DOMAIN" ]]; then + # check renew-date on ex_cert and compare to local ( if local exists) + enddate_ex=$(echo "$EX_CERT" | openssl x509 -noout -enddate 2>/dev/null| cut -d= -f 2-) + enddate_ex_s=$(date_epoc "$enddate_ex") + debug "external cert has enddate $enddate_ex ( $enddate_ex_s ) " + if [[ -s "$CERT_FILE" ]]; then # if local exists + enddate_lc=$(openssl x509 -noout -enddate < "$CERT_FILE" 2>/dev/null| cut -d= -f 2-) + enddate_lc_s=$(date_epoc "$enddate_lc") + debug "local cert has enddate $enddate_lc ( $enddate_lc_s ) " + else + enddate_lc_s=0 + debug "local cert doesn't exist" + fi + if [[ "$enddate_ex_s" -eq "$enddate_lc_s" ]]; then + debug "certificates expire at the same time" + elif [[ "$enddate_ex_s" -gt "$enddate_lc_s" ]]; then + # remote has longer to expiry date than local copy. + debug "remote cert has longer to run than local cert - ignoring" + else + info "${DOMAIN}: remote cert expires sooner than local, attempting to upload from local" + copy_file_to_location "domain certificate" \ + "$CERT_FILE" \ + "$DOMAIN_CERT_LOCATION" + copy_file_to_location "private key" \ + "$DOMAIN_DIR/${DOMAIN}.key" \ + "$DOMAIN_KEY_LOCATION" + copy_file_to_location "CA certificate" "$CA_CERT" "$CA_CERT_LOCATION" + cat "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}_chain.pem" + copy_file_to_location "full pem" \ + "$TEMP_DIR/${DOMAIN}_chain.pem" \ + "$DOMAIN_CHAIN_LOCATION" + cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" > "$TEMP_DIR/${DOMAIN}_K_C.pem" + copy_file_to_location "private key and domain cert pem" \ + "$TEMP_DIR/${DOMAIN}_K_C.pem" \ + "$DOMAIN_KEY_CERT_LOCATION" + cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}.pem" + copy_file_to_location "full pem" \ + "$TEMP_DIR/${DOMAIN}.pem" \ + "$DOMAIN_PEM_LOCATION" + reload_service + fi + else + info "${DOMAIN}: Certificate on remote domain does not match, ignoring remote certificate" + fi + fi + else + info "${DOMAIN}: no certificate obtained from host" + fi + # end of .... if obtained a cert +fi +# end of .... check_remote is true then connect and obtain the current certificate + +# if there is an existing certificate file, check details. +if [[ -s "$CERT_FILE" ]]; then + debug "certificate $CERT_FILE exists" + enddate=$(openssl x509 -in "$CERT_FILE" -noout -enddate 2>/dev/null| cut -d= -f 2-) + debug "local cert is valid until $enddate" + if [[ "$enddate" != "-" ]]; then + enddate_s=$(date_epoc "$enddate") + if [[ $(date_renew) -lt "$enddate_s" ]] && [[ $_FORCE_RENEW -ne 1 ]]; then + issuer=$(openssl x509 -in "$CERT_FILE" -noout -issuer 2>/dev/null) + if [[ "$issuer" == *"Fake LE Intermediate"* ]] && [[ "$CA" == "https://acme-v01.api.letsencrypt.org" ]]; then + debug "upgradeing from fake cert to real" + else + info "${DOMAIN}: certificate is valid for more than $RENEW_ALLOW days (until $enddate)" + # everything is OK, so exit. + graceful_exit + fi + else + debug "${DOMAIN}: certificate needs renewal" + fi + fi +fi +# end of .... if there is an existing certificate file, check details. + +if [[ ! -t 0 ]] && [[ "$PREVENT_NON_INTERACTIVE_RENEWAL" = "true" ]]; then + errmsg="$DOMAIN due for renewal," + errmsg="${errmsg} but not completed due to PREVENT_NON_INTERACTIVE_RENEWAL=true in config" + error_exit "$errmsg" +fi + +# create account key if it doesn't exist. +if [[ -s "$ACCOUNT_KEY" ]]; then + debug "Account key exists at $ACCOUNT_KEY skipping generation" +else + info "creating account key $ACCOUNT_KEY" + create_key "$ACCOUNT_KEY_TYPE" "$ACCOUNT_KEY" "$ACCOUNT_KEY_LENGTH" +fi + +# if not reusing priavte key, then remove the old keys +if [[ "$REUSE_PRIVATE_KEY" != "true" ]]; then + if [[ -s "$DOMAIN_DIR/${DOMAIN}.key" ]]; then + rm -f "$DOMAIN_DIR/${DOMAIN}.key" + fi + if [[ -s "$DOMAIN_DIR/${DOMAIN}.ec.key" ]]; then + rm -f "$DOMAIN_DIR/${DOMAIN}.ecs.key" + fi +fi +# create new domain keys if they don't already exist +if [[ "$DUAL_RSA_ECDSA" == "false" ]]; then + create_key "${PRIVATE_KEY_ALG}" "$DOMAIN_DIR/${DOMAIN}.key" "$DOMAIN_KEY_LENGTH" +else + create_key "rsa" "$DOMAIN_DIR/${DOMAIN}.key" "$DOMAIN_KEY_LENGTH" + create_key "${PRIVATE_KEY_ALG}" "$DOMAIN_DIR/${DOMAIN}.ec.key" "$DOMAIN_KEY_LENGTH" +fi +# End of creating domain keys. + +#create SAN +if [[ -z "$SANS" ]]; then + SANLIST="subjectAltName=DNS:${DOMAIN}" +elif [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then + SANLIST="subjectAltName=DNS:${SANS//,/,DNS:}" +else + SANLIST="subjectAltName=DNS:${DOMAIN},DNS:${SANS//,/,DNS:}" +fi +debug "created SAN list = $SANLIST" + +#create CSR's +if [[ "$DUAL_RSA_ECDSA" == "false" ]]; then + create_csr "$DOMAIN_DIR/${DOMAIN}.csr" "$DOMAIN_DIR/${DOMAIN}.key" +else + create_csr "$DOMAIN_DIR/${DOMAIN}.csr" "$DOMAIN_DIR/${DOMAIN}.key" + create_csr "$DOMAIN_DIR/${DOMAIN}.ec.csr" "$DOMAIN_DIR/${DOMAIN}.ec.key" +fi + +# use account key to register with CA +# currently the code registers every time, and gets an "already registered" back if it has been. +get_signing_params "$ACCOUNT_KEY" + +if [[ "$ACCOUNT_EMAIL" ]] ; then + regjson='{"resource": "new-reg", "contact": ["mailto: '$ACCOUNT_EMAIL'"], "agreement": "'$AGREEMENT'"}' +else + regjson='{"resource": "new-reg", "agreement": "'$AGREEMENT'"}' +fi + +info "Registering account" +# send the request to the ACME server. +send_signed_request "$URL_new_reg" "$regjson" + +if [[ "$code" == "" ]] || [[ "$code" == '201' ]] ; then + info "Registered" + echo "$response" > "$TEMP_DIR/account.json" +elif [[ "$code" == '409' ]] ; then + debug "Already registered" +else + error_exit "Error registering account ... $(json_get "$response" detail)" +fi +# end of registering account with CA + +# verify each domain +info "Verify each domain" + +# loop through domains for cert ( from SANS list) +if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then + alldomains=${SANS//,/ } +else + alldomains=$(echo "$DOMAIN,$SANS" | sed "s/,/ /g") +fi +dn=0 +for d in $alldomains; do + # $d is domain in current loop, which is number $dn for ACL + info "Verifying $d" + if [[ "$USE_SINGLE_ACL" == "true" ]]; then + DOMAIN_ACL="${ACL[0]}" + else + DOMAIN_ACL="${ACL[$dn]}" + fi + + # request a challenge token from ACME server + request="{\"resource\":\"new-authz\",\"identifier\":{\"type\":\"dns\",\"value\":\"$d\"}}" + send_signed_request "$URL_new_authz" "$request" + + debug "completed send_signed_request" + # check if we got a valid response and token, if not then error exit + if [[ ! -z "$code" ]] && [[ ! "$code" == '201' ]] ; then + error_exit "new-authz error: $response" + fi + + if [[ $response_status == "valid" ]]; then + info "$d is already validated" + if [[ "$DEACTIVATE_AUTH" == "true" ]]; then + deactivate_url="$(echo "$responseHeaders" | awk ' $1 ~ "^Location" {print $2}' | tr -d "\r")" + deactivate_url_list+=" $deactivate_url " + debug "url added to deactivate list ${deactivate_url}" + debug "deactivate list is now $deactivate_url_list" + fi + # increment domain-counter + ((dn++)) + else + PREVIOUSLY_VALIDATED="false" + if [[ $VALIDATE_VIA_DNS == "true" ]]; then # set up the correct DNS token for verification + # get the dns component of the ACME response + # get the token from the dns component + token=$(json_get "$response" "token" "dns-01") + debug token "$token" + # get the uri from the dns component + uri=$(json_get "$response" "uri" "dns-01") + debug uri "$uri" + + keyauthorization="$token.$thumbprint" + debug keyauthorization "$keyauthorization" + + #create signed authorization key from token. + auth_key=$(printf '%s' "$keyauthorization" | openssl dgst -sha256 -binary \ + | openssl base64 -e \ + | tr -d '\n\r' \ + | sed -e 's:=*$::g' -e 'y:+/:-_:') + debug auth_key "$auth_key" + + debug "adding dns via command: $DNS_ADD_COMMAND $d $auth_key" + if ! eval "$DNS_ADD_COMMAND" "$d" "$auth_key" ; then + error_exit "DNS_ADD_COMMAND failed for domain $d" + fi + + # find a primary / authoritative DNS server for the domain + if [[ -z "$AUTH_DNS_SERVER" ]]; then + get_auth_dns "$d" + else + primary_ns="$AUTH_DNS_SERVER" + fi + debug primary_ns "$primary_ns" + + # make a directory to hold pending dns-challenges + if [[ ! -d "$TEMP_DIR/dns_verify" ]]; then + mkdir "$TEMP_DIR/dns_verify" + fi + + # generate a file with the current variables for the dns-challenge + cat > "$TEMP_DIR/dns_verify/$d" <<- _EOF_ + token="${token}" + uri="${uri}" + keyauthorization="${keyauthorization}" + d="${d}" + primary_ns="${primary_ns}" + auth_key="${auth_key}" + _EOF_ + + else # set up the correct http token for verification + # get the token from the http component + token=$(json_get "$response" "token" "http-01") + debug token "$token" + # get the uri from the http component + uri=$(json_get "$response" "uri" "http-01") + debug uri "$uri" + + #create signed authorization key from token. + keyauthorization="$token.$thumbprint" + debug keyauthorization "$keyauthorization" + + # save variable into temporary file + echo -n "$keyauthorization" > "$TEMP_DIR/$token" + chmod 644 "$TEMP_DIR/$token" + + # copy to token to acme challenge location + umask 0022 + IFS=\; read -r -a token_locations <<<"$DOMAIN_ACL" + for t_loc in "${token_locations[@]}"; do + debug "copying file from $TEMP_DIR/$token to ${t_loc}" + copy_file_to_location "challenge token" \ + "$TEMP_DIR/$token" \ + "${t_loc}/$token" + done + umask "$ORIG_UMASK" + + wellknown_url="${CHALLENGE_CHECK_TYPE}://$d/.well-known/acme-challenge/$token" + debug wellknown_url "$wellknown_url" + + if [[ "$SKIP_HTTP_TOKEN_CHECK" == "true" ]]; then + info "SKIP_HTTP_TOKEN_CHECK=true so not checking that token is working correctly" + else + sleep "$HTTP_TOKEN_CHECK_WAIT" + # check that we can reach the challenge ourselves, if not, then error + if [[ ! "$(curl -k --silent --location "$wellknown_url")" == "$keyauthorization" ]]; then + error_exit "for some reason could not reach $wellknown_url - please check it manually" + fi + fi + + check_challenge_completion "$uri" "$d" "$keyauthorization" + + debug "remove token from ${DOMAIN_ACL}" + IFS=\; read -r -a token_locations <<<"$DOMAIN_ACL" + for t_loc in "${token_locations[@]}"; do + if [[ "${t_loc:0:4}" == "ssh:" ]] ; then + sshhost=$(echo "${t_loc}"| awk -F: '{print $2}') + command="rm -f ${t_loc:(( ${#sshhost} + 5))}/${token:?}" + debug "running following command to remove token" + debug "ssh $sshhost ${command}" + # shellcheck disable=SC2029 + ssh "$sshhost" "${command}" 1>/dev/null 2>&1 + rm -f "${TEMP_DIR:?}/${token:?}" + elif [[ "${t_loc:0:4}" == "ftp:" ]] ; then + debug "using ftp to remove token file" + ftpuser=$(echo "${t_loc}"| awk -F: '{print $2}') + ftppass=$(echo "${t_loc}"| awk -F: '{print $3}') + ftphost=$(echo "${t_loc}"| awk -F: '{print $4}') + ftplocn=$(echo "${t_loc}"| awk -F: '{print $5}') + debug "ftp user=$ftpuser - pass=$ftppass - host=$ftphost location=$ftplocn" + ftp -n <<- EOF + open $ftphost + user $ftpuser $ftppass + cd $ftplocn + delete ${token:?} + EOF + else + rm -f "${t_loc:?}/${token:?}" + fi + done + fi + # increment domain-counter + ((dn++)) + fi +done # end of ... loop through domains for cert ( from SANS list) + +# perform validation if via DNS challenge +if [[ $VALIDATE_VIA_DNS == "true" ]]; then + # loop through dns-variable files to check if dns has been changed + for dnsfile in $TEMP_DIR/dns_verify/*; do + if [[ -e "$dnsfile" ]]; then + debug "loading DNSfile: $dnsfile" + # shellcheck source=/dev/null + . "$dnsfile" + + # check for token at public dns server, waiting for a valid response. + for ns in $primary_ns; do + debug "checking dns at $ns" + ntries=0 + check_dns="fail" + while [[ "$check_dns" == "fail" ]]; do + if [[ "$os" == "cygwin" ]]; then + check_result=$(nslookup -type=txt "_acme-challenge.${d}" "${ns}" \ + | grep ^_acme -A2\ + | grep '"'|awk -F'"' '{ print $2}') + elif [[ "$DNS_CHECK_FUNC" == "drill" ]] || [[ "$DNS_CHECK_FUNC" == "dig" ]]; then + check_result=$($DNS_CHECK_FUNC TXT "_acme-challenge.${d}" "@${ns}" \ + | grep ^_acme|awk -F'"' '{ print $2}') + elif [[ "$DNS_CHECK_FUNC" == "host" ]]; then + check_result=$($DNS_CHECK_FUNC -t TXT "_acme-challenge.${d}" "${ns}" \ + | grep ^_acme|awk -F'"' '{ print $2}') + else + check_result=$(nslookup -type=txt "_acme-challenge.${d}" "${ns}" \ + | grep ^_acme|awk -F'"' '{ print $2}') + fi + debug "expecting $auth_key" + debug "${ns} gave ... $check_result" + + if [[ "$check_result" == *"$auth_key"* ]]; then + check_dns="success" + else + if [[ $ntries -lt 100 ]]; then + ntries=$(( ntries + 1 )) + info "checking DNS at ${ns} for ${d}. Attempt $ntries/100 gave wrong result, "\ + "waiting $DNS_WAIT secs before checking again" + sleep $DNS_WAIT + else + debug "dns check failed - removing existing value" + error_exit "checking _acme-challenge.${d} gave $check_result not $auth_key" + fi + fi + done + done + fi + done + + if [[ "$DNS_EXTRA_WAIT" -gt 0 && "$PREVIOUSLY_VALIDATED" != "true" ]]; then + info "sleeping $DNS_EXTRA_WAIT seconds before asking the ACME-server to check the dns" + sleep "$DNS_EXTRA_WAIT" + fi + + # loop through dns-variable files to let the ACME server check the challenges + for dnsfile in $TEMP_DIR/dns_verify/*; do + if [[ -e "$dnsfile" ]]; then + debug "loading DNSfile: $dnsfile" + # shellcheck source=/dev/null + . "$dnsfile" + + check_challenge_completion "$uri" "$d" "$keyauthorization" + + debug "remove DNS entry" + eval "$DNS_DEL_COMMAND" "$d" "$auth_key" + # remove $dnsfile after each loop. + rm -f "$dnsfile" + fi + done +fi +# end of ... perform validation if via DNS challenge +#end of varify each domain. + +# Verification has been completed for all SANS, so request certificate. +info "Verification completed, obtaining certificate." + +#obtain the certificate. +get_certificate "$DOMAIN_DIR/${DOMAIN}.csr" \ + "$CERT_FILE" \ + "$CA_CERT" +if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then + get_certificate "$DOMAIN_DIR/${DOMAIN}.ec.csr" \ + "${CERT_FILE::-4}.ec.crt" \ + "${CA_CERT::-4}.ec.crt" +fi + +# create Archive of new certs and keys. +cert_archive + +debug "Certificates obtained and archived locally, will now copy to specified locations" + +# copy certs to the correct location (creating concatenated files as required) + +copy_file_to_location "domain certificate" "$CERT_FILE" "$DOMAIN_CERT_LOCATION" +copy_file_to_location "private key" "$DOMAIN_DIR/${DOMAIN}.key" "$DOMAIN_KEY_LOCATION" +copy_file_to_location "CA certificate" "$CA_CERT" "$CA_CERT_LOCATION" +if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then + if [[ ! -z "$DOMAIN_CERT_LOCATION" ]]; then + copy_file_to_location "ec domain certificate" \ + "${CERT_FILE::-4}.ec.crt" \ + "${DOMAIN_CERT_LOCATION::-4}.ec.crt" + fi + if [[ ! -z "$DOMAIN_KEY_LOCATION" ]]; then + copy_file_to_location "ec private key" \ + "$DOMAIN_DIR/${DOMAIN}.ec.key" \ + "${DOMAIN_KEY_LOCATION::-4}.ec.key" + fi + if [[ ! -z "$CA_CERT_LOCATION" ]]; then + copy_file_to_location "ec CA certificate" \ + "${CA_CERT::-4}.ec.crt" \ + "${CA_CERT_LOCATION::-4}.ec.crt" + fi +fi + +# if DOMAIN_CHAIN_LOCATION is not blank, then create and copy file. +if [[ ! -z "$DOMAIN_CHAIN_LOCATION" ]]; then + if [[ "$(dirname "$DOMAIN_CHAIN_LOCATION")" == "." ]]; then + to_location="${DOMAIN_DIR}/${DOMAIN_CHAIN_LOCATION}" + else + to_location="${DOMAIN_CHAIN_LOCATION}" + fi + cat "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}_chain.pem" + copy_file_to_location "full chain" "$TEMP_DIR/${DOMAIN}_chain.pem" "$to_location" + if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then + cat "${CERT_FILE::-4}.ec.crt" "${CA_CERT::-4}.ec.crt" > "$TEMP_DIR/${DOMAIN}_chain.pem.ec" + copy_file_to_location "full chain" "$TEMP_DIR/${DOMAIN}_chain.pem.ec" "${to_location}.ec" + fi +fi +# if DOMAIN_KEY_CERT_LOCATION is not blank, then create and copy file. +if [[ ! -z "$DOMAIN_KEY_CERT_LOCATION" ]]; then + if [[ "$(dirname "$DOMAIN_KEY_CERT_LOCATION")" == "." ]]; then + to_location="${DOMAIN_DIR}/${DOMAIN_KEY_CERT_LOCATION}" + else + to_location="${DOMAIN_KEY_CERT_LOCATION}" + fi + cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" > "$TEMP_DIR/${DOMAIN}_K_C.pem" + copy_file_to_location "private key and domain cert pem" "$TEMP_DIR/${DOMAIN}_K_C.pem" "$to_location" + if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then + cat "$DOMAIN_DIR/${DOMAIN}.ec.key" "${CERT_FILE::-4}.ec.crt" > "$TEMP_DIR/${DOMAIN}_K_C.pem.ec" + copy_file_to_location "private ec key and domain cert pem" "$TEMP_DIR/${DOMAIN}_K_C.pem.ec" "${to_location}.ec" + fi +fi +# if DOMAIN_PEM_LOCATION is not blank, then create and copy file. +if [[ ! -z "$DOMAIN_PEM_LOCATION" ]]; then + if [[ "$(dirname "$DOMAIN_PEM_LOCATION")" == "." ]]; then + to_location="${DOMAIN_DIR}/${DOMAIN_PEM_LOCATION}" + else + to_location="${DOMAIN_PEM_LOCATION}" + fi + cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}.pem" + copy_file_to_location "full key, cert and chain pem" "$TEMP_DIR/${DOMAIN}.pem" "$to_location" + if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then + cat "$DOMAIN_DIR/${DOMAIN}.ec.key" "${CERT_FILE::-4}.ec.crt" "${CA_CERT::-4}.ec.crt" > "$TEMP_DIR/${DOMAIN}.pem.ec" + copy_file_to_location "full ec key, cert and chain pem" "$TEMP_DIR/${DOMAIN}.pem.ec" "${to_location}.ec" + fi +fi +# end of copying certs. + +# Run reload command to restart apache / nginx or whatever system +reload_service + +# deactivate authorizations +if [[ "$DEACTIVATE_AUTH" == "true" ]]; then + debug "in deactivate list is $deactivate_url_list" + for deactivate_url in $deactivate_url_list; do + resp=$(curl "$deactivate_url" 2>/dev/null) + d=$(json_get "$resp" "hostname") + info "deactivating domain $d" + debug "deactivating $deactivate_url" + send_signed_request "$deactivate_url" "{\"resource\": \"authz\", \"status\": \"deactivated\"}" + # check response + if [[ "$code" == "200" ]]; then + debug "Authorization deactivated" + else + error_exit "$domain: Deactivation error: $code" + fi + done +fi +# end of deactivating authorizations + +# Check if the certificate is installed correctly +if [[ ${CHECK_REMOTE} == "true" ]]; then + sleep "$CHECK_REMOTE_WAIT" + # shellcheck disable=SC2086 + CERT_REMOTE=$(echo \ + | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} 2>/dev/null \ + | openssl x509 -noout -fingerprint 2>/dev/null) + CERT_LOCAL=$(openssl x509 -noout -fingerprint < "$CERT_FILE" 2>/dev/null) + if [[ "$CERT_LOCAL" == "$CERT_REMOTE" ]]; then + info "${DOMAIN} - certificate installed OK on server" + else + error_exit "${DOMAIN} - certificate obtained but certificate on server is different from the new certificate" + fi +fi +# end of Check if the certificate is installed correctly + +# To have reached here, a certificate should have been successfully obtained. +# Use echo rather than info so that 'quiet' is ignored. +echo "certificate obtained for ${DOMAIN}" + +# gracefully exit ( tidying up temporary files etc). +graceful_exit From ded5d1654194e543567d440a4e8d642ba20d340b Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Mon, 25 Dec 2017 11:16:46 -0800 Subject: [PATCH 54/58] Fix typo in version --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7043a73..ecb2089 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,4 +1,4 @@ -versio: 2 +version: 2 jobs: build: docker: From 658ebc5943b7664dfa3005e6f83f62a062965de1 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Mon, 25 Dec 2017 11:18:28 -0800 Subject: [PATCH 55/58] Squashed '.bats-core/' content from commit b1da565 git-subtree-dir: .bats-core git-subtree-split: b1da565f92ad9dabf66831d0877542dcacc735b8 --- .appveyor.yml | 15 + .gitattributes | 3 + .travis.yml | 20 + CONDUCT.md | 83 ++++ Dockerfile | 8 + LICENSE | 41 ++ README.md | 354 +++++++++++++++++ bin/bats | 1 + install.sh | 48 +++ libexec/bats | 169 ++++++++ libexec/bats-exec-suite | 59 +++ libexec/bats-exec-test | 364 ++++++++++++++++++ libexec/bats-format-tap-stream | 173 +++++++++ libexec/bats-preprocess | 54 +++ man/Makefile | 10 + man/README.md | 5 + man/bats.1 | 103 +++++ man/bats.1.ronn | 110 ++++++ man/bats.7 | 178 +++++++++ man/bats.7.ronn | 156 ++++++++ package.json | 8 + test/bats.bats | 357 +++++++++++++++++ test/fixtures/bats/dos_line.bats | 3 + test/fixtures/bats/empty.bats | 0 test/fixtures/bats/environment.bats | 8 + .../bats/expand_var_in_test_name.bats | 3 + test/fixtures/bats/failing.bats | 5 + test/fixtures/bats/failing_and_passing.bats | 7 + test/fixtures/bats/failing_helper.bats | 6 + test/fixtures/bats/failing_setup.bats | 7 + test/fixtures/bats/failing_teardown.bats | 7 + test/fixtures/bats/intact.bats | 6 + test/fixtures/bats/invalid_tap.bats | 7 + test/fixtures/bats/load.bats | 6 + test/fixtures/bats/loop_keep_IFS.bats | 16 + test/fixtures/bats/output.bats | 19 + test/fixtures/bats/passing.bats | 3 + test/fixtures/bats/passing_and_failing.bats | 7 + test/fixtures/bats/passing_and_skipping.bats | 11 + .../bats/passing_failing_and_skipping.bats | 11 + .../bats/quoted_and_unquoted_test_names.bats | 11 + test/fixtures/bats/setup.bats | 17 + test/fixtures/bats/single_line.bats | 9 + test/fixtures/bats/skipped.bats | 7 + test/fixtures/bats/teardown.bats | 17 + test/fixtures/bats/test_helper.bash | 7 + .../bats/unofficial_bash_strict_mode.bash | 3 + .../bats/unofficial_bash_strict_mode.bats | 4 + test/fixtures/bats/whitespace.bats | 33 ++ .../bats/without_trailing_newline.bats | 3 + test/fixtures/suite/empty/.gitkeep | 0 test/fixtures/suite/multiple/a.bats | 3 + test/fixtures/suite/multiple/b.bats | 7 + test/fixtures/suite/single/test.bats | 3 + test/suite.bats | 64 +++ test/test_helper.bash | 27 ++ test/tmp/.gitignore | 2 + 57 files changed, 2668 insertions(+) create mode 100644 .appveyor.yml create mode 100755 .gitattributes create mode 100644 .travis.yml create mode 100644 CONDUCT.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 120000 bin/bats create mode 100755 install.sh create mode 100755 libexec/bats create mode 100755 libexec/bats-exec-suite create mode 100755 libexec/bats-exec-test create mode 100755 libexec/bats-format-tap-stream create mode 100755 libexec/bats-preprocess create mode 100644 man/Makefile create mode 100644 man/README.md create mode 100644 man/bats.1 create mode 100644 man/bats.1.ronn create mode 100644 man/bats.7 create mode 100644 man/bats.7.ronn create mode 100644 package.json create mode 100755 test/bats.bats create mode 100644 test/fixtures/bats/dos_line.bats create mode 100644 test/fixtures/bats/empty.bats create mode 100644 test/fixtures/bats/environment.bats create mode 100644 test/fixtures/bats/expand_var_in_test_name.bats create mode 100644 test/fixtures/bats/failing.bats create mode 100644 test/fixtures/bats/failing_and_passing.bats create mode 100644 test/fixtures/bats/failing_helper.bats create mode 100644 test/fixtures/bats/failing_setup.bats create mode 100644 test/fixtures/bats/failing_teardown.bats create mode 100644 test/fixtures/bats/intact.bats create mode 100644 test/fixtures/bats/invalid_tap.bats create mode 100644 test/fixtures/bats/load.bats create mode 100644 test/fixtures/bats/loop_keep_IFS.bats create mode 100644 test/fixtures/bats/output.bats create mode 100644 test/fixtures/bats/passing.bats create mode 100644 test/fixtures/bats/passing_and_failing.bats create mode 100644 test/fixtures/bats/passing_and_skipping.bats create mode 100644 test/fixtures/bats/passing_failing_and_skipping.bats create mode 100644 test/fixtures/bats/quoted_and_unquoted_test_names.bats create mode 100644 test/fixtures/bats/setup.bats create mode 100644 test/fixtures/bats/single_line.bats create mode 100644 test/fixtures/bats/skipped.bats create mode 100644 test/fixtures/bats/teardown.bats create mode 100644 test/fixtures/bats/test_helper.bash create mode 100644 test/fixtures/bats/unofficial_bash_strict_mode.bash create mode 100644 test/fixtures/bats/unofficial_bash_strict_mode.bats create mode 100644 test/fixtures/bats/whitespace.bats create mode 100644 test/fixtures/bats/without_trailing_newline.bats create mode 100644 test/fixtures/suite/empty/.gitkeep create mode 100644 test/fixtures/suite/multiple/a.bats create mode 100644 test/fixtures/suite/multiple/b.bats create mode 100644 test/fixtures/suite/single/test.bats create mode 100755 test/suite.bats create mode 100644 test/test_helper.bash create mode 100644 test/tmp/.gitignore diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 0000000..bc83024 --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,15 @@ +version: 'v0.4.0.{build}' + +build: off + +# This presumes that Git bash is installed at `C:\Program Files\Git` and the +# bash we're using is `C:\Program Files\Git\bin\bash.exe`. +# +# If instead it finds the Windows Subsystem for Linux bash at +# `C:\Windows\System32\bash.exe`, it will fail with an error like: +# /mnt/c/.../bats-core/test/test_helper.bash: line 1: +# syntax error near unexpected token `$'{\r'' +test_script: + - where bash + - bash --version + - bash -c 'time libexec/bats test' diff --git a/.gitattributes b/.gitattributes new file mode 100755 index 0000000..20cad1f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* text=auto +*.sh eol=lf +libexec/* eol=lf diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..01b205e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,20 @@ +language: bash + +os: +- linux +- osx + +services: + - docker + +script: +- | + bash -c 'time bin/bats --tap test' + if [[ "$TRAVIS_OS_NAME" == 'linux' ]]; then + docker build --tag bats:latest . + docker run -it bats:latest --tap /opt/bats/test + fi + +notifications: + email: + on_success: never diff --git a/CONDUCT.md b/CONDUCT.md new file mode 100644 index 0000000..5534377 --- /dev/null +++ b/CONDUCT.md @@ -0,0 +1,83 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting one of the project maintainers listed below. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Project Maintainers + +### bats-core organization: +* Bianca Tamayo <> + + +## Project Original Author(s) +* Sam Stephenson <> + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c918d3d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM alpine:3.6 + +COPY . /opt/bats/ + +RUN apk --no-cache add bash \ + && ln -s /opt/bats/libexec/bats /usr/sbin/bats + +ENTRYPOINT ["bats"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3ea5607 --- /dev/null +++ b/LICENSE @@ -0,0 +1,41 @@ +Copyright (c) 2017 Bianca Tamayo and bats-core organization + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +Copyright (c) 2014 Sam Stephenson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ef28ba3 --- /dev/null +++ b/README.md @@ -0,0 +1,354 @@ +## BATS-core: Bash Automated Testing System (2017) + +[![Build Status](https://travis-ci.org/bats-core/bats-core.svg?branch=master)](https://travis-ci.org/bats-core/bats-core) + +### Background: +### What is this repo? +**Tuesday, September 19, 2017:** This is a mirrored fork of [bats](https://github.com/sstephenson/bats), at [0360811](https://github.com/sstephenson/bats/commit/03608115df2071fff4eaaff1605768c275e5f81f). It was created via `git clone --bare` and `git push --mirror`. + +#### Why was it created? +The original bats repository needed new maintainers, and has not been actively maintained since 2013. While there were volunteers for maintainers, attempts to organize issues, and outstanding PRs, the lack of write-access to the repo hindered progress severely. + +## What's the plan and why? +The rough plan, originally [outlined here](https://github.com/sstephenson/bats/issues/150#issuecomment-323845404) is to create a new, mirrored mainline (this repo!). An excerpt: + +> **1. Roadmap 1.0:** +> There are already existing high-quality PRs, and often-requested features and issues, especially here at [#196](https://github.com/sstephenson/bats/issues/196). Leverage these and **consolidate into a single roadmap**. +> +>**2. Create or choose a fork or *mirror* of this repo to use as the new mainline:** +>Repoint existing PRs (whichever ones are possible) to the new mainline, get that repo to a stable 1.0. IMO we should create an organization and grant 2-3 people admin and write access. +> + +Doing it this way accomplishes two things: +1. Removes the dependency on the original maintainer +2. Enables collaboration and contribution flow again +3. Allows the possibility of merging back to original, or merging from original if or when the need arises +4. Prevents lock-out by giving administrative access to more than one person, increases transferability + +## Misc +- We are `#bats` on freenode + +--- +# Bats: Bash Automated Testing System + +Bats is a [TAP](http://testanything.org)-compliant testing framework +for Bash. It provides a simple way to verify that the UNIX programs +you write behave as expected. + +A Bats test file is a Bash script with special syntax for defining +test cases. Under the hood, each test case is just a function with a +description. + +```bash +#!/usr/bin/env bats + +@test "addition using bc" { + result="$(echo 2+2 | bc)" + [ "$result" -eq 4 ] +} + +@test "addition using dc" { + result="$(echo 2 2+p | dc)" + [ "$result" -eq 4 ] +} +``` + +Bats is most useful when testing software written in Bash, but you can +use it to test any UNIX program. + +Test cases consist of standard shell commands. Bats makes use of +Bash's `errexit` (`set -e`) option when running test cases. If every +command in the test case exits with a `0` status code (success), the +test passes. In this way, each line is an assertion of truth. + + +## Running tests + +To run your tests, invoke the `bats` interpreter with a path to a test +file. The file's test cases are run sequentially and in isolation. If +all the test cases pass, `bats` exits with a `0` status code. If there +are any failures, `bats` exits with a `1` status code. + +When you run Bats from a terminal, you'll see output as each test is +performed, with a check-mark next to the test's name if it passes or +an "X" if it fails. + + $ bats addition.bats + ✓ addition using bc + ✓ addition using dc + + 2 tests, 0 failures + +If Bats is not connected to a terminal—in other words, if you +run it from a continuous integration system, or redirect its output to +a file—the results are displayed in human-readable, machine-parsable +[TAP format](http://testanything.org). + +You can force TAP output from a terminal by invoking Bats with the +`--tap` option. + + $ bats --tap addition.bats + 1..2 + ok 1 addition using bc + ok 2 addition using dc + +### Test suites + +You can invoke the `bats` interpreter with multiple test file +arguments, or with a path to a directory containing multiple `.bats` +files. Bats will run each test file individually and aggregate the +results. If any test case fails, `bats` exits with a `1` status code. + + +## Writing tests + +Each Bats test file is evaluated _n+1_ times, where _n_ is the number of +test cases in the file. The first run counts the number of test cases, +then iterates over the test cases and executes each one in its own +process. + +For more details about how Bats evaluates test files, see +[Bats Evaluation Process](https://github.com/bats-core/bats-core/wiki/Bats-Evaluation-Process) +on the wiki. + +### `run`: Test other commands + +Many Bats tests need to run a command and then make assertions about +its exit status and output. Bats includes a `run` helper that invokes +its arguments as a command, saves the exit status and output into +special global variables, and then returns with a `0` status code so +you can continue to make assertions in your test case. + +For example, let's say you're testing that the `foo` command, when +passed a nonexistent filename, exits with a `1` status code and prints +an error message. + +```bash +@test "invoking foo with a nonexistent file prints an error" { + run foo nonexistent_filename + [ "$status" -eq 1 ] + [ "$output" = "foo: no such file 'nonexistent_filename'" ] +} +``` + +The `$status` variable contains the status code of the command, and +the `$output` variable contains the combined contents of the command's +standard output and standard error streams. + +A third special variable, the `$lines` array, is available for easily +accessing individual lines of output. For example, if you want to test +that invoking `foo` without any arguments prints usage information on +the first line: + +```bash +@test "invoking foo without arguments prints usage" { + run foo + [ "$status" -eq 1 ] + [ "${lines[0]}" = "usage: foo " ] +} +``` + +### `load`: Share common code + +You may want to share common code across multiple test files. Bats +includes a convenient `load` command for sourcing a Bash source file +relative to the location of the current test file. For example, if you +have a Bats test in `test/foo.bats`, the command + +```bash +load test_helper +``` + +will source the script `test/test_helper.bash` in your test file. This +can be useful for sharing functions to set up your environment or load +fixtures. + +### `skip`: Easily skip tests + +Tests can be skipped by using the `skip` command at the point in a +test you wish to skip. + +```bash +@test "A test I don't want to execute for now" { + skip + run foo + [ "$status" -eq 0 ] +} +``` + +Optionally, you may include a reason for skipping: + +```bash +@test "A test I don't want to execute for now" { + skip "This command will return zero soon, but not now" + run foo + [ "$status" -eq 0 ] +} +``` + +Or you can skip conditionally: + +```bash +@test "A test which should run" { + if [ foo != bar ]; then + skip "foo isn't bar" + fi + + run foo + [ "$status" -eq 0 ] +} +``` + +### `setup` and `teardown`: Pre- and post-test hooks + +You can define special `setup` and `teardown` functions, which run +before and after each test case, respectively. Use these to load +fixtures, set up your environment, and clean up when you're done. + +### Code outside of test cases + +You can include code in your test file outside of `@test` functions. +For example, this may be useful if you want to check for dependencies +and fail immediately if they're not present. However, any output that +you print in code outside of `@test`, `setup` or `teardown` functions +must be redirected to `stderr` (`>&2`). Otherwise, the output may +cause Bats to fail by polluting the TAP stream on `stdout`. + +### Special variables + +There are several global variables you can use to introspect on Bats +tests: + +* `$BATS_TEST_FILENAME` is the fully expanded path to the Bats test +file. +* `$BATS_TEST_DIRNAME` is the directory in which the Bats test file is +located. +* `$BATS_TEST_NAMES` is an array of function names for each test case. +* `$BATS_TEST_NAME` is the name of the function containing the current +test case. +* `$BATS_TEST_DESCRIPTION` is the description of the current test +case. +* `$BATS_TEST_NUMBER` is the (1-based) index of the current test case +in the test file. +* `$BATS_TMPDIR` is the location to a directory that may be used to +store temporary files. + + +## Installing Bats from source + +Check out a copy of the Bats repository. Then, either add the Bats +`bin` directory to your `$PATH`, or run the provided `install.sh` +command with the location to the prefix in which you want to install +Bats. For example, to install Bats into `/usr/local`, + + $ git clone https://github.com/bats-core/bats-core.git + $ cd bats-core + $ ./install.sh /usr/local + +Note that you may need to run `install.sh` with `sudo` if you do not +have permission to write to the installation prefix. + +## Running Bats in Docker + +Check out a copy of the Bats repository, then build a container image: + + $ git clone https://github.com/bats-core/bats-core.git + $ cd bats-core + $ docker build --tag bats:latest . + +This creates a local Docker image called `bats:latest` based on [Alpine +Linux](https://github.com/gliderlabs/docker-alpine/blob/master/docs/usage.md). +To run Bats' internal test suite (which is in the container image at +`/opt/bats/test`): + + $ docker run -it bats:latest /opt/bats/test + +To run a test suite from your local machine, mount in a volume and direct +Bats to its path inside the container: + + $ docker run -it -v "$(pwd):/code" bats:latest /code/test + +This is a minimal image. If more tools are required this can be used as a +base image in a Dockerfile using `FROM `. +In the future there may be images based on Debian, and/or with more tools +installed (`curl` and `openssl`, for example). If you require a specific +configuration please search and +1 an issue or +[raise a new issue](https://github.com/bats-core/bats-core/issues). + +## Support + +The Bats source code repository is [hosted on +GitHub](https://github.com/bats-core/bats-core). There you can file bugs +on the issue tracker or submit tested pull requests for review. + +For real-world examples from open-source projects using Bats, see +[Projects Using Bats](https://github.com/bats-core/bats-core/wiki/Projects-Using-Bats) +on the wiki. + +To learn how to set up your editor for Bats syntax highlighting, see +[Syntax Highlighting](https://github.com/bats-core/bats-core/wiki/Syntax-Highlighting) +on the wiki. + + +## Version history + +*0.4.0* (August 13, 2014) + +* Improved the display of failing test cases. Bats now shows the + source code of failing test lines, along with full stack traces + including function names, filenames, and line numbers. +* Improved the display of the pretty-printed test summary line to + include the number of skipped tests, if any. +* Improved the speed of the preprocessor, dramatically shortening test + and suite startup times. +* Added support for absolute pathnames to the `load` helper. +* Added support for single-line `@test` definitions. +* Added bats(1) and bats(7) manual pages. +* Modified the `bats` command to default to TAP output when the `$CI` + variable is set, to better support environments such as Travis CI. + +*0.3.1* (October 28, 2013) + +* Fixed an incompatibility with the pretty formatter in certain + environments such as tmux. +* Fixed a bug where the pretty formatter would crash if the first line + of a test file's output was invalid TAP. + +*0.3.0* (October 21, 2013) + +* Improved formatting for tests run from a terminal. Failing tests + are now colored in red, and the total number of failing tests is + displayed at the end of the test run. When Bats is not connected to + a terminal (e.g. in CI runs), or when invoked with the `--tap` flag, + output is displayed in standard TAP format. +* Added the ability to skip tests using the `skip` command. +* Added a message to failing test case output indicating the file and + line number of the statement that caused the test to fail. +* Added "ad-hoc" test suite support. You can now invoke `bats` with + multiple filename or directory arguments to run all the specified + tests in aggregate. +* Added support for test files with Windows line endings. +* Fixed regular expression warnings from certain versions of Bash. +* Fixed a bug running tests containing lines that begin with `-e`. + +*0.2.0* (November 16, 2012) + +* Added test suite support. The `bats` command accepts a directory + name containing multiple test files to be run in aggregate. +* Added the ability to count the number of test cases in a file or + suite by passing the `-c` flag to `bats`. +* Preprocessed sources are cached between test case runs in the same + file for better performance. + +*0.1.0* (December 30, 2011) + +* Initial public release. + +--- + +© 2017 Bianca Tamayo (bats-core organization) + +© 2014 Sam Stephenson + +Bats is released under an MIT-style license; +see `LICENSE` for details. diff --git a/bin/bats b/bin/bats new file mode 120000 index 0000000..a50a884 --- /dev/null +++ b/bin/bats @@ -0,0 +1 @@ +../libexec/bats \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..b5a133d --- /dev/null +++ b/install.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +set -e + +resolve_link() { + $(type -p greadlink readlink | head -n1) "$1" +} + +abs_dirname() { + local cwd="$(pwd)" + local path="$1" + + while [ -n "$path" ]; do + cd "${path%/*}" + local name="${path##*/}" + path="$(resolve_link "$name" || true)" + done + + pwd + cd "$cwd" +} + +PREFIX="$1" +if [ -z "$1" ]; then + { echo "usage: $0 " + echo " e.g. $0 /usr/local" + } >&2 + exit 1 +fi + +BATS_ROOT="$(abs_dirname "$0")" +mkdir -p "$PREFIX"/{bin,libexec,share/man/man{1,7}} +cp -R "$BATS_ROOT"/bin/* "$PREFIX"/bin +cp -R "$BATS_ROOT"/libexec/* "$PREFIX"/libexec +cp "$BATS_ROOT"/man/bats.1 "$PREFIX"/share/man/man1 +cp "$BATS_ROOT"/man/bats.7 "$PREFIX"/share/man/man7 + +# fix broken symbolic link file +if [ ! -L "$PREFIX"/bin/bats ]; then + dir="$(readlink -e "$PREFIX")" + rm -f "$dir"/bin/bats + ln -s "$dir"/libexec/bats "$dir"/bin/bats +fi + +# fix file permission +chmod a+x "$PREFIX"/bin/* +chmod a+x "$PREFIX"/libexec/* + +echo "Installed Bats to $PREFIX/bin/bats" diff --git a/libexec/bats b/libexec/bats new file mode 100755 index 0000000..9dd32dc --- /dev/null +++ b/libexec/bats @@ -0,0 +1,169 @@ +#!/usr/bin/env bash +set -e + +version() { + echo "Bats 0.4.0" +} + +usage() { + version + echo "Usage: bats [-c] [-p | -t] [ ...]" +} + +help() { + usage + echo + echo " is the path to a Bats test file, or the path to a directory" + echo " containing Bats test files." + echo + echo " -c, --count Count the number of test cases without running any tests" + echo " -h, --help Display this help message" + echo " -p, --pretty Show results in pretty format (default for terminals)" + echo " -t, --tap Show results in TAP format" + echo " -v, --version Display the version number" + echo + echo " For more information, see https://github.com/bats-core/bats-core" + echo +} + +BATS_READLINK= + +resolve_link() { + if [[ -z "$BATS_READLINK" ]]; then + if command -v 'greadlink' >/dev/null; then + BATS_READLINK='greadlink' + elif command -v 'readlink' >/dev/null; then + BATS_READLINK='readlink' + else + BATS_READLINK='true' + fi + fi + "$BATS_READLINK" "$1" || return 0 +} + +abs_dirname() { + local cwd="$PWD" + local path="$1" + + while [ -n "$path" ]; do + cd "${path%/*}" + local name="${path##*/}" + path="$(resolve_link "$name")" + done + + printf -v "$2" -- '%s' "$PWD" + cd "$cwd" +} + +expand_path() { + local path="${1%/}" + local dirname="${path%/*}" + + if [[ "$dirname" == "$path" ]]; then + dirname="$PWD" + elif cd "$dirname" 2>/dev/null; then + dirname="$PWD" + cd "$OLDPWD" + else + printf '%s' "$path" + return + fi + printf -v "$2" '%s/%s' "$dirname" "${path##*/}" +} + +abs_dirname "$0" 'BATS_LIBEXEC' +abs_dirname "$BATS_LIBEXEC" 'BATS_PREFIX' +abs_dirname '.' 'BATS_CWD' + +export BATS_PREFIX +export BATS_CWD +export BATS_TEST_PATTERN="^[[:blank:]]*@test[[:blank:]]+(.*[^[:blank:]])[[:blank:]]+\{(.*)\$" +export PATH="$BATS_LIBEXEC:$PATH" + +options=() +arguments=() +for arg in "$@"; do + if [ "${arg:0:1}" = "-" ]; then + if [ "${arg:1:1}" = "-" ]; then + options[${#options[*]}]="${arg:2}" + else + index=1 + while option="${arg:$index:1}"; do + [ -n "$option" ] || break + options[${#options[*]}]="$option" + let index+=1 + done + fi + else + arguments[${#arguments[*]}]="$arg" + fi +done + +unset count_flag pretty +count_flag='' +pretty='' +[ -t 0 ] && [ -t 1 ] && pretty="1" +[ -n "${CI:-}" ] && pretty="" + +if [[ "${#options[@]}" -ne '0' ]]; then + for option in "${options[@]}"; do + case "$option" in + "h" | "help" ) + help + exit 0 + ;; + "v" | "version" ) + version + exit 0 + ;; + "c" | "count" ) + count_flag="-c" + ;; + "t" | "tap" ) + pretty="" + ;; + "p" | "pretty" ) + pretty="1" + ;; + * ) + usage >&2 + exit 1 + ;; + esac + done +fi + +if [ "${#arguments[@]}" -eq 0 ]; then + usage >&2 + exit 1 +fi + +filenames=() +for filename in "${arguments[@]}"; do + expand_path "$filename" 'filename' + + if [ -d "$filename" ]; then + shopt -s nullglob + for suite_filename in "$filename"/*.bats; do + filenames["${#filenames[@]}"]="$suite_filename" + done + shopt -u nullglob + else + filenames["${#filenames[@]}"]="$filename" + fi +done + +if [ "${#filenames[@]}" -eq 1 ]; then + command="bats-exec-test" +else + command="bats-exec-suite" +fi + +set -o pipefail execfail +if [ -z "$pretty" ]; then + exec "$command" $count_flag "${filenames[@]}" +else + extended_syntax_flag="-x" + formatter="bats-format-tap-stream" + exec "$command" $count_flag $extended_syntax_flag "${filenames[@]}" | "$formatter" +fi diff --git a/libexec/bats-exec-suite b/libexec/bats-exec-suite new file mode 100755 index 0000000..96021bb --- /dev/null +++ b/libexec/bats-exec-suite @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -e + +count_only_flag="" +if [ "$1" = "-c" ]; then + count_only_flag=1 + shift +fi + +extended_syntax_flag="" +if [ "$1" = "-x" ]; then + extended_syntax_flag="-x" + shift +fi + +trap "kill 0; exit 1" int + +count=0 +for filename in "$@"; do + while IFS= read -r line; do + if [[ "$line" =~ $BATS_TEST_PATTERN ]]; then + let count+=1 + fi + done <"$filename" +done + +if [ -n "$count_only_flag" ]; then + echo "$count" + exit +fi + +echo "1..$count" +status=0 +offset=0 +for filename in "$@"; do + index=0 + { + IFS= read -r # 1..n + while IFS= read -r line; do + case "$line" in + "begin "* ) + let index+=1 + echo "${line/ $index / $(($offset + $index)) }" + ;; + "ok "* | "not ok "* ) + [ -n "$extended_syntax_flag" ] || let index+=1 + echo "${line/ $index / $(($offset + $index)) }" + [ "${line:0:6}" != "not ok" ] || status=1 + ;; + * ) + echo "$line" + ;; + esac + done + } < <( bats-exec-test $extended_syntax_flag "$filename" ) + offset=$(($offset + $index)) +done + +exit "$status" diff --git a/libexec/bats-exec-test b/libexec/bats-exec-test new file mode 100755 index 0000000..ef87057 --- /dev/null +++ b/libexec/bats-exec-test @@ -0,0 +1,364 @@ +#!/usr/bin/env bash +set -e +set -E +set -T + +BATS_COUNT_ONLY="" +if [ "$1" = "-c" ]; then + BATS_COUNT_ONLY=1 + shift +fi + +BATS_EXTENDED_SYNTAX="" +if [ "$1" = "-x" ]; then + BATS_EXTENDED_SYNTAX="$1" + shift +fi + +BATS_TEST_FILENAME="$1" +if [ -z "$BATS_TEST_FILENAME" ]; then + echo "usage: bats-exec " >&2 + exit 1 +elif [ ! -f "$BATS_TEST_FILENAME" ]; then + echo "bats: $BATS_TEST_FILENAME does not exist" >&2 + exit 1 +else + shift +fi + +BATS_TEST_DIRNAME="${BATS_TEST_FILENAME%/*}" +BATS_TEST_NAMES=() + +load() { + local name="$1" + local filename + + if [ "${name:0:1}" = "/" ]; then + filename="${name}" + else + filename="$BATS_TEST_DIRNAME/${name}.bash" + fi + + if [[ ! -f "$filename" ]]; then + echo "bats: $filename does not exist" >&2 + exit 1 + fi + + source "${filename}" +} + +run() { + local e E T oldIFS + [[ ! "$-" =~ e ]] || e=1 + [[ ! "$-" =~ E ]] || E=1 + [[ ! "$-" =~ T ]] || T=1 + set +e + set +E + set +T + output="$("$@" 2>&1)" + status="$?" + oldIFS=$IFS + IFS=$'\n' lines=($output) + [ -z "$e" ] || set -e + [ -z "$E" ] || set -E + [ -z "$T" ] || set -T + IFS=$oldIFS +} + +setup() { + true +} + +teardown() { + true +} + +BATS_TEST_SKIPPED='' +skip() { + BATS_TEST_SKIPPED=${1:-1} + BATS_TEST_COMPLETED=1 + exit 0 +} + +bats_test_begin() { + BATS_TEST_DESCRIPTION="$1" + if [ -n "$BATS_EXTENDED_SYNTAX" ]; then + echo "begin $BATS_TEST_NUMBER $BATS_TEST_DESCRIPTION" >&3 + fi + setup +} + +bats_test_function() { + local test_name="$1" + BATS_TEST_NAMES+=("$test_name") +} + +BATS_CURRENT_STACK_TRACE=() +BATS_PREVIOUS_STACK_TRACE=() + +bats_capture_stack_trace() { + if [[ "${#BATS_CURRENT_STACK_TRACE[@]}" -ne '0' ]]; then + BATS_PREVIOUS_STACK_TRACE=("${BATS_CURRENT_STACK_TRACE[@]}") + fi + BATS_CURRENT_STACK_TRACE=() + + local test_pattern=" $BATS_TEST_NAME $BATS_TEST_SOURCE" + local setup_pattern=" setup $BATS_TEST_SOURCE" + local teardown_pattern=" teardown $BATS_TEST_SOURCE" + + local frame + local i + + for ((i=2; i != ${#FUNCNAME[@]}; ++i)); do + frame="${BASH_LINENO[$((i-1))]} ${FUNCNAME[$i]} ${BASH_SOURCE[$i]}" + BATS_CURRENT_STACK_TRACE["${#BATS_CURRENT_STACK_TRACE[@]}"]="$frame" + if [[ "$frame" = *"$test_pattern" || \ + "$frame" = *"$setup_pattern" || \ + "$frame" = *"$teardown_pattern" ]]; then + break + fi + done + + bats_frame_filename "${BATS_CURRENT_STACK_TRACE[0]}" 'BATS_SOURCE' + bats_frame_lineno "${BATS_CURRENT_STACK_TRACE[0]}" 'BATS_LINENO' +} + +bats_print_stack_trace() { + local frame + local index=1 + local count="${#@}" + local filename + local lineno + + for frame in "$@"; do + bats_frame_filename "$frame" 'filename' + bats_trim_filename "$filename" 'filename' + bats_frame_lineno "$frame" 'lineno' + + if [ $index -eq 1 ]; then + echo -n "# (" + else + echo -n "# " + fi + + local fn + bats_frame_function "$frame" 'fn' + if [ "$fn" != "$BATS_TEST_NAME" ]; then + echo -n "from function \`$fn' " + fi + + if [ $index -eq $count ]; then + echo "in test file $filename, line $lineno)" + else + echo "in file $filename, line $lineno," + fi + + let index+=1 + done +} + +bats_print_failed_command() { + local frame="$1" + local status="$2" + local filename + local lineno + local failed_line + local failed_command + + bats_frame_filename "$frame" 'filename' + bats_frame_lineno "$frame" 'lineno' + bats_extract_line "$filename" "$lineno" 'failed_line' + bats_strip_string "$failed_line" 'failed_command' + printf '%s' "# \`${failed_command}' " + + if [ $status -eq 1 ]; then + echo "failed" + else + echo "failed with status $status" + fi +} + +bats_frame_lineno() { + printf -v "$2" '%s' "${1%% *}" +} + +bats_frame_function() { + local __bff_function="${1#* }" + printf -v "$2" '%s' "${__bff_function%% *}" +} + +bats_frame_filename() { + local __bff_filename="${1#* }" + __bff_filename="${__bff_filename#* }" + + if [ "$__bff_filename" = "$BATS_TEST_SOURCE" ]; then + __bff_filename="$BATS_TEST_FILENAME" + fi + printf -v "$2" '%s' "$__bff_filename" +} + +bats_extract_line() { + local __bats_extract_line_line + local __bats_extract_line_index='0' + + while IFS= read -r __bats_extract_line_line; do + if [[ "$((++__bats_extract_line_index))" -eq "$2" ]]; then + printf -v "$3" '%s' "${__bats_extract_line_line%$'\r'}" + break + fi + done <"$1" +} + +bats_strip_string() { + [[ "$1" =~ ^[[:space:]]*(.*)[[:space:]]*$ ]] + printf -v "$2" '%s' "${BASH_REMATCH[1]}" +} + +bats_trim_filename() { + if [[ "$1" =~ ^${BATS_CWD}/ ]]; then + printf -v "$2" '%s' "${1#$BATS_CWD/}" + else + printf -v "$2" '%s' "$1" + fi +} + +bats_debug_trap() { + if [ "$BASH_SOURCE" != "$1" ]; then + bats_capture_stack_trace + fi +} + +# When running under Bash 3.2.57(1)-release on macOS, the `ERR` trap may not +# always fire, but the `EXIT` trap will. For this reason we call it at the very +# beginning of `bats_teardown_trap` (the `DEBUG` trap for the call will move +# `BATS_CURRENT_STACK_TRACE` to `BATS_PREVIOUS_STACK_TRACE`) and check the value +# of `$?` before taking other actions. +bats_error_trap() { + local status="$?" + if [[ "$status" -ne '0' ]]; then + BATS_ERROR_STATUS="$status" + BATS_ERROR_STACK_TRACE=( "${BATS_PREVIOUS_STACK_TRACE[@]}" ) + trap - debug + fi +} + +bats_teardown_trap() { + bats_error_trap + trap "bats_exit_trap" exit + local status=0 + teardown >>"$BATS_OUT" 2>&1 || status="$?" + + if [ $status -eq 0 ]; then + BATS_TEARDOWN_COMPLETED=1 + elif [ -n "$BATS_TEST_COMPLETED" ]; then + BATS_ERROR_STATUS="$status" + BATS_ERROR_STACK_TRACE=( "${BATS_CURRENT_STACK_TRACE[@]}" ) + fi + + bats_exit_trap +} + +bats_exit_trap() { + local status + local skipped='' + trap - err exit + + if [ -n "$BATS_TEST_SKIPPED" ]; then + skipped=" # skip" + if [ "1" != "$BATS_TEST_SKIPPED" ]; then + skipped+=" $BATS_TEST_SKIPPED" + fi + fi + + if [ -z "$BATS_TEST_COMPLETED" ] || [ -z "$BATS_TEARDOWN_COMPLETED" ]; then + echo "not ok $BATS_TEST_NUMBER $BATS_TEST_DESCRIPTION" >&3 + bats_print_stack_trace "${BATS_ERROR_STACK_TRACE[@]}" >&3 + bats_print_failed_command "${BATS_ERROR_STACK_TRACE[${#BATS_ERROR_STACK_TRACE[@]}-1]}" "$BATS_ERROR_STATUS" >&3 + sed -e "s/^/# /" < "$BATS_OUT" >&3 + status=1 + else + echo "ok ${BATS_TEST_NUMBER} ${BATS_TEST_DESCRIPTION}${skipped}" >&3 + status=0 + fi + + rm -f "$BATS_OUT" + exit "$status" +} + +bats_perform_tests() { + echo "1..$#" + test_number=1 + status=0 + for test_name in "$@"; do + "$0" $BATS_EXTENDED_SYNTAX "$BATS_TEST_FILENAME" "$test_name" "$test_number" || status=1 + let test_number+=1 + done + exit "$status" +} + +bats_perform_test() { + BATS_TEST_NAME="$1" + if declare -F "$BATS_TEST_NAME" >/dev/null; then + BATS_TEST_NUMBER="$2" + if [ -z "$BATS_TEST_NUMBER" ]; then + echo "1..1" + BATS_TEST_NUMBER="1" + fi + + BATS_TEST_COMPLETED="" + BATS_TEARDOWN_COMPLETED="" + trap "bats_debug_trap \"\$BASH_SOURCE\"" debug + trap "bats_error_trap" err + trap "bats_teardown_trap" exit + "$BATS_TEST_NAME" >>"$BATS_OUT" 2>&1 + BATS_TEST_COMPLETED=1 + + else + echo "bats: unknown test name \`$BATS_TEST_NAME'" >&2 + exit 1 + fi +} + +if [ -z "$TMPDIR" ]; then + BATS_TMPDIR="/tmp" +else + BATS_TMPDIR="${TMPDIR%/}" +fi + +BATS_TMPNAME="$BATS_TMPDIR/bats.$$" +BATS_PARENT_TMPNAME="$BATS_TMPDIR/bats.$PPID" +BATS_OUT="${BATS_TMPNAME}.out" + +bats_preprocess_source() { + BATS_TEST_SOURCE="${BATS_TMPNAME}.src" + . bats-preprocess <<< "$(< "$BATS_TEST_FILENAME")"$'\n' > "$BATS_TEST_SOURCE" + trap "bats_cleanup_preprocessed_source" err exit + trap "bats_cleanup_preprocessed_source; exit 1" int +} + +bats_cleanup_preprocessed_source() { + rm -f "$BATS_TEST_SOURCE" +} + +bats_evaluate_preprocessed_source() { + if [ -z "$BATS_TEST_SOURCE" ]; then + BATS_TEST_SOURCE="${BATS_PARENT_TMPNAME}.src" + fi + source "$BATS_TEST_SOURCE" +} + +exec 3<&1 + +if [ "$#" -eq 0 ]; then + bats_preprocess_source + bats_evaluate_preprocessed_source + + if [ -n "$BATS_COUNT_ONLY" ]; then + echo "${#BATS_TEST_NAMES[@]}" + else + bats_perform_tests "${BATS_TEST_NAMES[@]}" + fi +else + bats_evaluate_preprocessed_source + bats_perform_test "$@" +fi diff --git a/libexec/bats-format-tap-stream b/libexec/bats-format-tap-stream new file mode 100755 index 0000000..5d876cf --- /dev/null +++ b/libexec/bats-format-tap-stream @@ -0,0 +1,173 @@ +#!/usr/bin/env bash +set -e + +# Just stream the TAP output (sans extended syntax) if tput is missing +command -v tput >/dev/null || exec grep -v "^begin " + +header_pattern='[0-9]+\.\.[0-9]+' +IFS= read -r header + +if [[ "$header" =~ $header_pattern ]]; then + count="${header:3}" + index=0 + failures=0 + skipped=0 + name="" + count_column_width=$(( ${#count} * 2 + 2 )) +else + # If the first line isn't a TAP plan, print it and pass the rest through + printf "%s\n" "$header" + exec cat +fi + +update_screen_width() { + screen_width="$(tput cols)" + count_column_left=$(( $screen_width - $count_column_width )) +} + +trap update_screen_width WINCH +update_screen_width + +begin() { + go_to_column 0 + printf_with_truncation $(( $count_column_left - 1 )) " %s" "$name" + clear_to_end_of_line + go_to_column $count_column_left + printf "%${#count}s/${count}" "$index" + go_to_column 1 +} + +pass() { + go_to_column 0 + printf " ✓ %s" "$name" + advance +} + +skip() { + local reason="$1" + [ -z "$reason" ] || reason=": $reason" + go_to_column 0 + printf " - %s (skipped%s)" "$name" "$reason" + advance +} + +fail() { + go_to_column 0 + set_color 1 bold + printf " ✗ %s" "$name" + advance +} + +log() { + set_color 1 + printf " %s\n" "$1" + clear_color +} + +summary() { + printf "\n%d test" "$count" + if [[ "$count" -ne '1' ]]; then + printf 's' + fi + + printf ", %d failure" "$failures" + if [[ "$failures" -ne '1' ]]; then + printf 's' + fi + + if [ "$skipped" -gt 0 ]; then + printf ", %d skipped" "$skipped" + fi + + printf "\n" +} + +printf_with_truncation() { + local width="$1" + shift + local string + + printf -v 'string' -- "$@" + + if [ "${#string}" -gt "$width" ]; then + printf "%s..." "${string:0:$(( $width - 4 ))}" + else + printf "%s" "$string" + fi +} + +go_to_column() { + local column="$1" + printf "\x1B[%dG" $(( $column + 1 )) +} + +clear_to_end_of_line() { + printf "\x1B[K" +} + +advance() { + clear_to_end_of_line + echo + clear_color +} + +set_color() { + local color="$1" + local weight='22' + + if [[ "$2" == 'bold' ]]; then + weight='1' + fi + printf "\x1B[%d;%dm" $(( 30 + $color )) "$weight" +} + +clear_color() { + printf "\x1B[0m" +} + +_buffer="" + +buffer() { + _buffer="${_buffer}$("$@")" +} + +flush() { + printf "%s" "$_buffer" + _buffer="" +} + +finish() { + flush + printf "\n" +} + +trap finish EXIT + +while IFS= read -r line; do + case "$line" in + "begin "* ) + let index+=1 + name="${line#* $index }" + buffer begin + flush + ;; + "ok "* ) + skip_expr="ok $index (.*) # skip ?(([^)]*))?" + if [[ "$line" =~ $skip_expr ]]; then + let skipped+=1 + buffer skip "${BASH_REMATCH[2]}" + else + buffer pass + fi + ;; + "not ok "* ) + let failures+=1 + buffer fail + ;; + "# "* ) + buffer log "${line:2}" + ;; + esac +done + +buffer summary diff --git a/libexec/bats-preprocess b/libexec/bats-preprocess new file mode 100755 index 0000000..8307b76 --- /dev/null +++ b/libexec/bats-preprocess @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -e + +encode_name() { + local name="$1" + local result="test_" + local hex_code + + if [[ ! "$name" =~ [^[:alnum:]\ _-] ]]; then + name="${name//_/-5f}" + name="${name//-/-2d}" + name="${name// /_}" + result+="$name" + else + local length="${#name}" + local char i + + for ((i=0; i [ ...] + + is the path to a Bats test file, or the path to a directory +containing Bats test files. + + +DESCRIPTION +----------- + +Bats is a TAP-compliant testing framework for Bash. It provides a simple +way to verify that the UNIX programs you write behave as expected. + +A Bats test file is a Bash script with special syntax for defining +test cases. Under the hood, each test case is just a function with a +description. + +Test cases consist of standard shell commands. Bats makes use of +Bash's `errexit` (`set -e`) option when running test cases. If every +command in the test case exits with a `0` status code (success), the +test passes. In this way, each line is an assertion of truth. + +See `bats`(7) for more information on writing Bats tests. + + +RUNNING TESTS +------------- + +To run your tests, invoke the `bats` interpreter with a path to a test +file. The file's test cases are run sequentially and in isolation. If +all the test cases pass, `bats` exits with a `0` status code. If there +are any failures, `bats` exits with a `1` status code. + +You can invoke the `bats` interpreter with multiple test file arguments, +or with a path to a directory containing multiple `.bats` files. Bats +will run each test file individually and aggregate the results. If any +test case fails, `bats` exits with a `1` status code. + + +OPTIONS +------- + + * `-c`, `--count`: + Count the number of test cases without running any tests + * `-h`, `--help`: + Display help message + * `-p`, `--pretty`: + Show results in pretty format (default for terminals) + * `-t`, `--tap`: + Show results in TAP format + * `-v`, `--version`: + Display the version number + + +OUTPUT +------ + +When you run Bats from a terminal, you'll see output as each test is +performed, with a check-mark next to the test's name if it passes or +an "X" if it fails. + + $ bats addition.bats + ✓ addition using bc + ✓ addition using dc + + 2 tests, 0 failures + +If Bats is not connected to a terminal--in other words, if you run it +from a continuous integration system or redirect its output to a +file--the results are displayed in human-readable, machine-parsable +TAP format. You can force TAP output from a terminal by invoking Bats +with the `--tap` option. + + $ bats --tap addition.bats + 1..2 + ok 1 addition using bc + ok 2 addition using dc + + +EXIT STATUS +----------- + +The `bats` interpreter exits with a value of `0` if all test cases pass, +or `1` if one or more test cases fail. + + +SEE ALSO +-------- + +Bats wiki: _https://github.com/bats\-core/bats\-core/wiki/_ + +`bash`(1), `bats`(7) + + +COPYRIGHT +--------- + +(c) 2017 Bianca Tamayo (bats-core organization) +(c) 2014 Sam Stephenson + +Bats is released under the terms of an MIT-style license. + + + diff --git a/man/bats.7 b/man/bats.7 new file mode 100644 index 0000000..d0836e5 --- /dev/null +++ b/man/bats.7 @@ -0,0 +1,178 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BATS" "7" "November 2013" "" "" +. +.SH "NAME" +\fBbats\fR \- Bats test file format +. +.SH "DESCRIPTION" +A Bats test file is a Bash script with special syntax for defining test cases\. Under the hood, each test case is just a function with a description\. +. +.IP "" 4 +. +.nf + +#!/usr/bin/env bats + +@test "addition using bc" { + result="$(echo 2+2 | bc)" + [ "$result" \-eq 4 ] +} + +@test "addition using dc" { + result="$(echo 2 2+p | dc)" + [ "$result" \-eq 4 ] +} +. +.fi +. +.IP "" 0 +. +.P +Each Bats test file is evaluated n+1 times, where \fIn\fR is the number of test cases in the file\. The first run counts the number of test cases, then iterates over the test cases and executes each one in its own process\. +. +.SH "THE RUN HELPER" +Many Bats tests need to run a command and then make assertions about its exit status and output\. Bats includes a \fBrun\fR helper that invokes its arguments as a command, saves the exit status and output into special global variables, and then returns with a \fB0\fR status code so you can continue to make assertions in your test case\. +. +.P +For example, let\'s say you\'re testing that the \fBfoo\fR command, when passed a nonexistent filename, exits with a \fB1\fR status code and prints an error message\. +. +.IP "" 4 +. +.nf + +@test "invoking foo with a nonexistent file prints an error" { + run foo nonexistent_filename + [ "$status" \-eq 1 ] + [ "$output" = "foo: no such file \'nonexistent_filename\'" ] +} +. +.fi +. +.IP "" 0 +. +.P +The \fB$status\fR variable contains the status code of the command, and the \fB$output\fR variable contains the combined contents of the command\'s standard output and standard error streams\. +. +.P +A third special variable, the \fB$lines\fR array, is available for easily accessing individual lines of output\. For example, if you want to test that invoking \fBfoo\fR without any arguments prints usage information on the first line: +. +.IP "" 4 +. +.nf + +@test "invoking foo without arguments prints usage" { + run foo + [ "$status" \-eq 1 ] + [ "${lines[0]}" = "usage: foo " ] +} +. +.fi +. +.IP "" 0 +. +.SH "THE LOAD COMMAND" +You may want to share common code across multiple test files\. Bats includes a convenient \fBload\fR command for sourcing a Bash source file relative to the location of the current test file\. For example, if you have a Bats test in \fBtest/foo\.bats\fR, the command +. +.IP "" 4 +. +.nf + +load test_helper +. +.fi +. +.IP "" 0 +. +.P +will source the script \fBtest/test_helper\.bash\fR in your test file\. This can be useful for sharing functions to set up your environment or load fixtures\. +. +.SH "THE SKIP COMMAND" +Tests can be skipped by using the \fBskip\fR command at the point in a test you wish to skip\. +. +.IP "" 4 +. +.nf + +@test "A test I don\'t want to execute for now" { + skip + run foo + [ "$status" \-eq 0 ] +} +. +.fi +. +.IP "" 0 +. +.P +Optionally, you may include a reason for skipping: +. +.IP "" 4 +. +.nf + +@test "A test I don\'t want to execute for now" { + skip "This command will return zero soon, but not now" + run foo + [ "$status" \-eq 0 ] +} +. +.fi +. +.IP "" 0 +. +.P +Or you can skip conditionally: +. +.IP "" 4 +. +.nf + +@test "A test which should run" { + if [ foo != bar ]; then + skip "foo isn\'t bar" + fi + + run foo + [ "$status" \-eq 0 ] +} +. +.fi +. +.IP "" 0 +. +.SH "SETUP AND TEARDOWN FUNCTIONS" +You can define special \fBsetup\fR and \fBteardown\fR functions which run before and after each test case, respectively\. Use these to load fixtures, set up your environment, and clean up when you\'re done\. +. +.SH "CODE OUTSIDE OF TEST CASES" +You can include code in your test file outside of \fB@test\fR functions\. For example, this may be useful if you want to check for dependencies and fail immediately if they\'re not present\. However, any output that you print in code outside of \fB@test\fR, \fBsetup\fR or \fBteardown\fR functions must be redirected to \fBstderr\fR (\fB>&2\fR)\. Otherwise, the output may cause Bats to fail by polluting the TAP stream on \fBstdout\fR\. +. +.SH "SPECIAL VARIABLES" +There are several global variables you can use to introspect on Bats tests: +. +.IP "\(bu" 4 +\fB$BATS_TEST_FILENAME\fR is the fully expanded path to the Bats test file\. +. +.IP "\(bu" 4 +\fB$BATS_TEST_DIRNAME\fR is the directory in which the Bats test file is located\. +. +.IP "\(bu" 4 +\fB$BATS_TEST_NAMES\fR is an array of function names for each test case\. +. +.IP "\(bu" 4 +\fB$BATS_TEST_NAME\fR is the name of the function containing the current test case\. +. +.IP "\(bu" 4 +\fB$BATS_TEST_DESCRIPTION\fR is the description of the current test case\. +. +.IP "\(bu" 4 +\fB$BATS_TEST_NUMBER\fR is the (1\-based) index of the current test case in the test file\. +. +.IP "\(bu" 4 +\fB$BATS_TMPDIR\fR is the location to a directory that may be used to store temporary files\. +. +.IP "" 0 +. +.SH "SEE ALSO" +\fBbash\fR(1), \fBbats\fR(1) diff --git a/man/bats.7.ronn b/man/bats.7.ronn new file mode 100644 index 0000000..7f6dd18 --- /dev/null +++ b/man/bats.7.ronn @@ -0,0 +1,156 @@ +bats(7) -- Bats test file format +================================ + + +DESCRIPTION +----------- + +A Bats test file is a Bash script with special syntax for defining +test cases. Under the hood, each test case is just a function with a +description. + + #!/usr/bin/env bats + + @test "addition using bc" { + result="$(echo 2+2 | bc)" + [ "$result" -eq 4 ] + } + + @test "addition using dc" { + result="$(echo 2 2+p | dc)" + [ "$result" -eq 4 ] + } + + +Each Bats test file is evaluated n+1 times, where _n_ is the number of +test cases in the file. The first run counts the number of test cases, +then iterates over the test cases and executes each one in its own +process. + + +THE RUN HELPER +-------------- + +Many Bats tests need to run a command and then make assertions about +its exit status and output. Bats includes a `run` helper that invokes +its arguments as a command, saves the exit status and output into +special global variables, and then returns with a `0` status code so +you can continue to make assertions in your test case. + +For example, let's say you're testing that the `foo` command, when +passed a nonexistent filename, exits with a `1` status code and prints +an error message. + + @test "invoking foo with a nonexistent file prints an error" { + run foo nonexistent_filename + [ "$status" -eq 1 ] + [ "$output" = "foo: no such file 'nonexistent_filename'" ] + } + +The `$status` variable contains the status code of the command, and +the `$output` variable contains the combined contents of the command's +standard output and standard error streams. + +A third special variable, the `$lines` array, is available for easily +accessing individual lines of output. For example, if you want to test +that invoking `foo` without any arguments prints usage information on +the first line: + + @test "invoking foo without arguments prints usage" { + run foo + [ "$status" -eq 1 ] + [ "${lines[0]}" = "usage: foo " ] + } + + +THE LOAD COMMAND +---------------- + +You may want to share common code across multiple test files. Bats +includes a convenient `load` command for sourcing a Bash source file +relative to the location of the current test file. For example, if you +have a Bats test in `test/foo.bats`, the command + + load test_helper + +will source the script `test/test_helper.bash` in your test file. This +can be useful for sharing functions to set up your environment or load +fixtures. + + +THE SKIP COMMAND +---------------- + +Tests can be skipped by using the `skip` command at the point in a +test you wish to skip. + + @test "A test I don't want to execute for now" { + skip + run foo + [ "$status" -eq 0 ] + } + +Optionally, you may include a reason for skipping: + + @test "A test I don't want to execute for now" { + skip "This command will return zero soon, but not now" + run foo + [ "$status" -eq 0 ] + } + +Or you can skip conditionally: + + @test "A test which should run" { + if [ foo != bar ]; then + skip "foo isn't bar" + fi + + run foo + [ "$status" -eq 0 ] + } + + +SETUP AND TEARDOWN FUNCTIONS +---------------------------- + +You can define special `setup` and `teardown` functions which run +before and after each test case, respectively. Use these to load +fixtures, set up your environment, and clean up when you're done. + + +CODE OUTSIDE OF TEST CASES +-------------------------- + +You can include code in your test file outside of `@test` functions. +For example, this may be useful if you want to check for dependencies +and fail immediately if they're not present. However, any output that +you print in code outside of `@test`, `setup` or `teardown` functions +must be redirected to `stderr` (`>&2`). Otherwise, the output may +cause Bats to fail by polluting the TAP stream on `stdout`. + + +SPECIAL VARIABLES +----------------- + +There are several global variables you can use to introspect on Bats +tests: + +* `$BATS_TEST_FILENAME` is the fully expanded path to the Bats test +file. +* `$BATS_TEST_DIRNAME` is the directory in which the Bats test file is +located. +* `$BATS_TEST_NAMES` is an array of function names for each test case. +* `$BATS_TEST_NAME` is the name of the function containing the current +test case. +* `$BATS_TEST_DESCRIPTION` is the description of the current test +case. +* `$BATS_TEST_NUMBER` is the (1-based) index of the current test case +in the test file. +* `$BATS_TMPDIR` is the location to a directory that may be used to +store temporary files. + + +SEE ALSO +-------- + +`bash`(1), `bats`(1) diff --git a/package.json b/package.json new file mode 100644 index 0000000..6bcff0c --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "name": "bats", + "version": "v0.4.0", + "description": "Bash Automated Testing System", + "install": "./install.sh ${PREFIX:-/usr/local}", + "scripts": [ "libexec/bats", "libexec/bats-exec-suite", "libexec/bats-exec-test", "libexec/bats-format-tap-stream", "libexec/bats-preprocess", "bin/bats" ] +} + diff --git a/test/bats.bats b/test/bats.bats new file mode 100755 index 0000000..89cacbf --- /dev/null +++ b/test/bats.bats @@ -0,0 +1,357 @@ +#!/usr/bin/env bats + +load test_helper +fixtures bats + +@test "no arguments prints usage instructions" { + run bats + [ $status -eq 1 ] + [ $(expr "${lines[1]}" : "Usage:") -ne 0 ] +} + +@test "-v and --version print version number" { + run bats -v + [ $status -eq 0 ] + [ $(expr "$output" : "Bats [0-9][0-9.]*") -ne 0 ] +} + +@test "-h and --help print help" { + run bats -h + [ $status -eq 0 ] + [ "${#lines[@]}" -gt 3 ] +} + +@test "invalid filename prints an error" { + run bats nonexistent + [ $status -eq 1 ] + [ $(expr "$output" : ".*does not exist") -ne 0 ] +} + +@test "empty test file runs zero tests" { + run bats "$FIXTURE_ROOT/empty.bats" + [ $status -eq 0 ] + [ "$output" = "1..0" ] +} + +@test "one passing test" { + run bats "$FIXTURE_ROOT/passing.bats" + [ $status -eq 0 ] + [ "${lines[0]}" = "1..1" ] + [ "${lines[1]}" = "ok 1 a passing test" ] +} + +@test "summary passing tests" { + run filter_control_sequences bats -p "$FIXTURE_ROOT/passing.bats" + [ $status -eq 0 ] + [ "${lines[1]}" = "1 test, 0 failures" ] +} + +@test "summary passing and skipping tests" { + run filter_control_sequences bats -p "$FIXTURE_ROOT/passing_and_skipping.bats" + [ $status -eq 0 ] + [ "${lines[3]}" = "3 tests, 0 failures, 2 skipped" ] +} + +@test "tap passing and skipping tests" { + run filter_control_sequences bats --tap "$FIXTURE_ROOT/passing_and_skipping.bats" + [ $status -eq 0 ] + [ "${lines[0]}" = "1..3" ] + [ "${lines[1]}" = "ok 1 a passing test" ] + [ "${lines[2]}" = "ok 2 a skipped test with no reason # skip" ] + [ "${lines[3]}" = "ok 3 a skipped test with a reason # skip for a really good reason" ] +} + +@test "summary passing and failing tests" { + run filter_control_sequences bats -p "$FIXTURE_ROOT/failing_and_passing.bats" + [ $status -eq 0 ] + [ "${lines[4]}" = "2 tests, 1 failure" ] +} + +@test "summary passing, failing and skipping tests" { + run filter_control_sequences bats -p "$FIXTURE_ROOT/passing_failing_and_skipping.bats" + [ $status -eq 0 ] + [ "${lines[5]}" = "3 tests, 1 failure, 1 skipped" ] +} + +@test "tap passing, failing and skipping tests" { + run filter_control_sequences bats --tap "$FIXTURE_ROOT/passing_failing_and_skipping.bats" + [ $status -eq 0 ] + [ "${lines[0]}" = "1..3" ] + [ "${lines[1]}" = "ok 1 a passing test" ] + [ "${lines[2]}" = "ok 2 a skipping test # skip" ] + [ "${lines[3]}" = "not ok 3 a failing test" ] +} + +@test "one failing test" { + run bats "$FIXTURE_ROOT/failing.bats" + [ $status -eq 1 ] + emit_debug_output + [ "${lines[0]}" = '1..1' ] + [ "${lines[1]}" = 'not ok 1 a failing test' ] + [ "${lines[2]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/failing.bats, line 4)" ] + [ "${lines[3]}" = "# \`eval \"( exit \${STATUS:-1} )\"' failed" ] +} + +@test "one failing and one passing test" { + run bats "$FIXTURE_ROOT/failing_and_passing.bats" + [ $status -eq 1 ] + [ "${lines[0]}" = '1..2' ] + [ "${lines[1]}" = 'not ok 1 a failing test' ] + [ "${lines[2]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/failing_and_passing.bats, line 2)" ] + [ "${lines[3]}" = "# \`false' failed" ] + [ "${lines[4]}" = 'ok 2 a passing test' ] +} + +@test "failing test with significant status" { + STATUS=2 run bats "$FIXTURE_ROOT/failing.bats" + [ $status -eq 1 ] + emit_debug_output + [ "${lines[3]}" = "# \`eval \"( exit \${STATUS:-1} )\"' failed with status 2" ] +} + +@test "failing helper function logs the test case's line number" { + run bats "$FIXTURE_ROOT/failing_helper.bats" + [ $status -eq 1 ] + [ "${lines[1]}" = 'not ok 1 failing helper function' ] + [ "${lines[2]}" = "# (from function \`failing_helper' in file $RELATIVE_FIXTURE_ROOT/test_helper.bash, line 6," ] + [ "${lines[3]}" = "# in test file $RELATIVE_FIXTURE_ROOT/failing_helper.bats, line 5)" ] + [ "${lines[4]}" = "# \`failing_helper' failed" ] +} + +@test "test environments are isolated" { + run bats "$FIXTURE_ROOT/environment.bats" + [ $status -eq 0 ] +} + +@test "setup is run once before each test" { + rm -f "$TMP/setup.log" + run bats "$FIXTURE_ROOT/setup.bats" + [ $status -eq 0 ] + run cat "$TMP/setup.log" + [ ${#lines[@]} -eq 3 ] +} + +@test "teardown is run once after each test, even if it fails" { + rm -f "$TMP/teardown.log" + run bats "$FIXTURE_ROOT/teardown.bats" + [ $status -eq 1 ] + run cat "$TMP/teardown.log" + [ ${#lines[@]} -eq 3 ] +} + +@test "setup failure" { + run bats "$FIXTURE_ROOT/failing_setup.bats" + [ $status -eq 1 ] + [ "${lines[1]}" = 'not ok 1 truth' ] + [ "${lines[2]}" = "# (from function \`setup' in test file $RELATIVE_FIXTURE_ROOT/failing_setup.bats, line 2)" ] + [ "${lines[3]}" = "# \`false' failed" ] +} + +@test "passing test with teardown failure" { + PASS=1 run bats "$FIXTURE_ROOT/failing_teardown.bats" + [ $status -eq 1 ] + [ "${lines[1]}" = 'not ok 1 truth' ] + [ "${lines[2]}" = "# (from function \`teardown' in test file $RELATIVE_FIXTURE_ROOT/failing_teardown.bats, line 2)" ] + [ "${lines[3]}" = "# \`eval \"( exit \${STATUS:-1} )\"' failed" ] +} + +@test "failing test with teardown failure" { + PASS=0 run bats "$FIXTURE_ROOT/failing_teardown.bats" + [ $status -eq 1 ] + [ "${lines[1]}" = 'not ok 1 truth' ] + [ "${lines[2]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/failing_teardown.bats, line 6)" ] + [ "${lines[3]}" = $'# `[ "$PASS" = "1" ]\' failed' ] +} + +@test "teardown failure with significant status" { + PASS=1 STATUS=2 run bats "$FIXTURE_ROOT/failing_teardown.bats" + [ $status -eq 1 ] + [ "${lines[3]}" = "# \`eval \"( exit \${STATUS:-1} )\"' failed with status 2" ] +} + +@test "failing test file outside of BATS_CWD" { + cd "$TMP" + run bats "$FIXTURE_ROOT/failing.bats" + [ $status -eq 1 ] + emit_debug_output + [ "${lines[2]}" = "# (in test file $FIXTURE_ROOT/failing.bats, line 4)" ] +} + +@test "load sources scripts relative to the current test file" { + run bats "$FIXTURE_ROOT/load.bats" + [ $status -eq 0 ] +} + +@test "load aborts if the specified script does not exist" { + HELPER_NAME="nonexistent" run bats "$FIXTURE_ROOT/load.bats" + [ $status -eq 1 ] +} + +@test "load sources scripts by absolute path" { + HELPER_NAME="${FIXTURE_ROOT}/test_helper.bash" run bats "$FIXTURE_ROOT/load.bats" + [ $status -eq 0 ] +} + +@test "load aborts if the script, specified by an absolute path, does not exist" { + HELPER_NAME="${FIXTURE_ROOT}/nonexistent" run bats "$FIXTURE_ROOT/load.bats" + [ $status -eq 1 ] +} + +@test "output is discarded for passing tests and printed for failing tests" { + run bats "$FIXTURE_ROOT/output.bats" + [ $status -eq 1 ] + [ "${lines[6]}" = '# failure stdout 1' ] + [ "${lines[7]}" = '# failure stdout 2' ] + [ "${lines[11]}" = '# failure stderr' ] +} + +@test "-c prints the number of tests" { + run bats -c "$FIXTURE_ROOT/empty.bats" + [ $status -eq 0 ] + [ "$output" = "0" ] + + run bats -c "$FIXTURE_ROOT/output.bats" + [ $status -eq 0 ] + [ "$output" = "4" ] +} + +@test "dash-e is not mangled on beginning of line" { + run bats "$FIXTURE_ROOT/intact.bats" + [ $status -eq 0 ] + [ "${lines[1]}" = "ok 1 dash-e on beginning of line" ] +} + +@test "dos line endings are stripped before testing" { + run bats "$FIXTURE_ROOT/dos_line.bats" + [ $status -eq 0 ] +} + +@test "test file without trailing newline" { + run bats "$FIXTURE_ROOT/without_trailing_newline.bats" + [ $status -eq 0 ] + [ "${lines[1]}" = "ok 1 truth" ] +} + +@test "skipped tests" { + run bats "$FIXTURE_ROOT/skipped.bats" + [ $status -eq 0 ] + [ "${lines[1]}" = "ok 1 a skipped test # skip" ] + [ "${lines[2]}" = "ok 2 a skipped test with a reason # skip a reason" ] +} + +@test "extended syntax" { + run bats-exec-test -x "$FIXTURE_ROOT/failing_and_passing.bats" + [ $status -eq 1 ] + [ "${lines[1]}" = 'begin 1 a failing test' ] + [ "${lines[2]}" = 'not ok 1 a failing test' ] + [ "${lines[5]}" = 'begin 2 a passing test' ] + [ "${lines[6]}" = 'ok 2 a passing test' ] +} + +@test "pretty and tap formats" { + run bats --tap "$FIXTURE_ROOT/passing.bats" + tap_output="$output" + [ $status -eq 0 ] + + run bats --pretty "$FIXTURE_ROOT/passing.bats" + pretty_output="$output" + [ $status -eq 0 ] + + [ "$tap_output" != "$pretty_output" ] +} + +@test "pretty formatter bails on invalid tap" { + run bats --tap "$FIXTURE_ROOT/invalid_tap.bats" + [ $status -eq 1 ] + [ "${lines[0]}" = "This isn't TAP!" ] + [ "${lines[1]}" = "Good day to you" ] +} + +@test "single-line tests" { + run bats "$FIXTURE_ROOT/single_line.bats" + [ $status -eq 1 ] + [ "${lines[1]}" = 'ok 1 empty' ] + [ "${lines[2]}" = 'ok 2 passing' ] + [ "${lines[3]}" = 'ok 3 input redirection' ] + [ "${lines[4]}" = 'not ok 4 failing' ] + [ "${lines[5]}" = "# (in test file $RELATIVE_FIXTURE_ROOT/single_line.bats, line 9)" ] + [ "${lines[6]}" = $'# `@test "failing" { false; }\' failed' ] +} + +@test "testing IFS not modified by run" { + run bats "$FIXTURE_ROOT/loop_keep_IFS.bats" + [ $status -eq 0 ] + [ "${lines[1]}" = "ok 1 loop_func" ] +} + +@test "expand variables in test name" { + SUITE='test/suite' run bats "$FIXTURE_ROOT/expand_var_in_test_name.bats" + [ $status -eq 0 ] + [ "${lines[1]}" = "ok 1 test/suite: test with variable in name" ] +} + +@test "handle quoted and unquoted test names" { + run bats "$FIXTURE_ROOT/quoted_and_unquoted_test_names.bats" + [ $status -eq 0 ] + [ "${lines[1]}" = "ok 1 single-quoted name" ] + [ "${lines[2]}" = "ok 2 double-quoted name" ] + [ "${lines[3]}" = "ok 3 unquoted name" ] +} + +@test 'ensure compatibility with unofficial Bash strict mode' { + local expected='ok 1 unofficial Bash strict mode conditions met' + + # Run Bats under `set -u` to catch as many unset variable accesses as + # possible. + run bash -u "${BATS_TEST_DIRNAME%/*}/libexec/bats" \ + "$FIXTURE_ROOT/unofficial_bash_strict_mode.bats" + if [[ "$status" -ne '0' || "${lines[1]}" != "$expected" ]]; then + cat <&2 +} + +@test "failure writing to stdout" { + echo "failure stdout 1" + echo "failure stdout 2" + false +} + +@test "failure writing to stderr" { + echo "failure stderr" >&2 + false +} diff --git a/test/fixtures/bats/passing.bats b/test/fixtures/bats/passing.bats new file mode 100644 index 0000000..e8182ce --- /dev/null +++ b/test/fixtures/bats/passing.bats @@ -0,0 +1,3 @@ +@test "a passing test" { + true +} diff --git a/test/fixtures/bats/passing_and_failing.bats b/test/fixtures/bats/passing_and_failing.bats new file mode 100644 index 0000000..7b7d8ee --- /dev/null +++ b/test/fixtures/bats/passing_and_failing.bats @@ -0,0 +1,7 @@ +@test "a passing test" { + true +} + +@test "a failing test" { + false +} diff --git a/test/fixtures/bats/passing_and_skipping.bats b/test/fixtures/bats/passing_and_skipping.bats new file mode 100644 index 0000000..83fc76b --- /dev/null +++ b/test/fixtures/bats/passing_and_skipping.bats @@ -0,0 +1,11 @@ +@test "a passing test" { + true +} + +@test "a skipped test with no reason" { + skip +} + +@test "a skipped test with a reason" { + skip "for a really good reason" +} diff --git a/test/fixtures/bats/passing_failing_and_skipping.bats b/test/fixtures/bats/passing_failing_and_skipping.bats new file mode 100644 index 0000000..3c9f17f --- /dev/null +++ b/test/fixtures/bats/passing_failing_and_skipping.bats @@ -0,0 +1,11 @@ +@test "a passing test" { + true +} + +@test "a skipping test" { + skip +} + +@test "a failing test" { + false +} diff --git a/test/fixtures/bats/quoted_and_unquoted_test_names.bats b/test/fixtures/bats/quoted_and_unquoted_test_names.bats new file mode 100644 index 0000000..aa460da --- /dev/null +++ b/test/fixtures/bats/quoted_and_unquoted_test_names.bats @@ -0,0 +1,11 @@ +@test 'single-quoted name' { + true +} + +@test "double-quoted name" { + true +} + +@test unquoted name { + true +} diff --git a/test/fixtures/bats/setup.bats b/test/fixtures/bats/setup.bats new file mode 100644 index 0000000..3cc52cc --- /dev/null +++ b/test/fixtures/bats/setup.bats @@ -0,0 +1,17 @@ +LOG="$TMP/setup.log" + +setup() { + echo "$BATS_TEST_NAME" >> "$LOG" +} + +@test "one" { + [ "$(tail -n 1 "$LOG")" = "test_one" ] +} + +@test "two" { + [ "$(tail -n 1 "$LOG")" = "test_two" ] +} + +@test "three" { + [ "$(tail -n 1 "$LOG")" = "test_three" ] +} diff --git a/test/fixtures/bats/single_line.bats b/test/fixtures/bats/single_line.bats new file mode 100644 index 0000000..fc342d9 --- /dev/null +++ b/test/fixtures/bats/single_line.bats @@ -0,0 +1,9 @@ +@test "empty" { } + +@test "passing" { true; } + +@test "input redirection" { diff - <( echo hello ); } <> "$LOG" +} + +@test "one" { + true +} + +@test "two" { + false +} + +@test "three" { + true +} diff --git a/test/fixtures/bats/test_helper.bash b/test/fixtures/bats/test_helper.bash new file mode 100644 index 0000000..530d034 --- /dev/null +++ b/test/fixtures/bats/test_helper.bash @@ -0,0 +1,7 @@ +help_me() { + true +} + +failing_helper() { + false +} diff --git a/test/fixtures/bats/unofficial_bash_strict_mode.bash b/test/fixtures/bats/unofficial_bash_strict_mode.bash new file mode 100644 index 0000000..3695e6f --- /dev/null +++ b/test/fixtures/bats/unofficial_bash_strict_mode.bash @@ -0,0 +1,3 @@ +#! /usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' diff --git a/test/fixtures/bats/unofficial_bash_strict_mode.bats b/test/fixtures/bats/unofficial_bash_strict_mode.bats new file mode 100644 index 0000000..473f3e4 --- /dev/null +++ b/test/fixtures/bats/unofficial_bash_strict_mode.bats @@ -0,0 +1,4 @@ +load unofficial_bash_strict_mode +@test "unofficial Bash strict mode conditions met" { + : +} diff --git a/test/fixtures/bats/whitespace.bats b/test/fixtures/bats/whitespace.bats new file mode 100644 index 0000000..1574085 --- /dev/null +++ b/test/fixtures/bats/whitespace.bats @@ -0,0 +1,33 @@ +@test "no extra whitespace" { + : +} + + @test "tab at beginning of line" { + : + } + +@test "tab before description" { + : +} + +@test "tab before opening brace" { + : +} + + @test "tabs at beginning of line and before description" { + : + } + + @test "tabs at beginning, before description, before brace" { + : + } + + @test "extra whitespace around single-line test" { :; } + +@test "no extra whitespace around single-line test" {:;} + +@test parse unquoted name between extra whitespace {:;} + +@test { {:;} # unquote single brace is a valid description + +@test ' {:;} # empty name from single quote diff --git a/test/fixtures/bats/without_trailing_newline.bats b/test/fixtures/bats/without_trailing_newline.bats new file mode 100644 index 0000000..e3ace8b --- /dev/null +++ b/test/fixtures/bats/without_trailing_newline.bats @@ -0,0 +1,3 @@ +@test "truth" { + true +} \ No newline at end of file diff --git a/test/fixtures/suite/empty/.gitkeep b/test/fixtures/suite/empty/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/suite/multiple/a.bats b/test/fixtures/suite/multiple/a.bats new file mode 100644 index 0000000..fbc1f38 --- /dev/null +++ b/test/fixtures/suite/multiple/a.bats @@ -0,0 +1,3 @@ +@test "truth" { + true +} diff --git a/test/fixtures/suite/multiple/b.bats b/test/fixtures/suite/multiple/b.bats new file mode 100644 index 0000000..bb965a4 --- /dev/null +++ b/test/fixtures/suite/multiple/b.bats @@ -0,0 +1,7 @@ +@test "more truth" { + true +} + +@test "quasi-truth" { + [ -z "$FLUNK" ] +} diff --git a/test/fixtures/suite/single/test.bats b/test/fixtures/suite/single/test.bats new file mode 100644 index 0000000..e8182ce --- /dev/null +++ b/test/fixtures/suite/single/test.bats @@ -0,0 +1,3 @@ +@test "a passing test" { + true +} diff --git a/test/suite.bats b/test/suite.bats new file mode 100755 index 0000000..5371686 --- /dev/null +++ b/test/suite.bats @@ -0,0 +1,64 @@ +#!/usr/bin/env bats + +load test_helper +fixtures suite + +@test "running a suite with no test files" { + run bats "$FIXTURE_ROOT/empty" + [ $status -eq 0 ] + [ "$output" = "1..0" ] +} + +@test "running a suite with one test file" { + run bats "$FIXTURE_ROOT/single" + [ $status -eq 0 ] + [ "${lines[0]}" = "1..1" ] + [ "${lines[1]}" = "ok 1 a passing test" ] +} + +@test "counting tests in a suite" { + run bats -c "$FIXTURE_ROOT/single" + [ $status -eq 0 ] + [ "$output" -eq 1 ] + + run bats -c "$FIXTURE_ROOT/multiple" + [ $status -eq 0 ] + [ "$output" -eq 3 ] +} + +@test "aggregated output of multiple tests in a suite" { + run bats "$FIXTURE_ROOT/multiple" + [ $status -eq 0 ] + [ "${lines[0]}" = "1..3" ] + echo "$output" | grep "^ok . truth" + echo "$output" | grep "^ok . more truth" + echo "$output" | grep "^ok . quasi-truth" +} + +@test "a failing test in a suite results in an error exit code" { + FLUNK=1 run bats "$FIXTURE_ROOT/multiple" + [ $status -eq 1 ] + [ "${lines[0]}" = "1..3" ] + echo "$output" | grep "^not ok . quasi-truth" +} + +@test "running an ad-hoc suite by specifying multiple test files" { + run bats "$FIXTURE_ROOT/multiple/a.bats" "$FIXTURE_ROOT/multiple/b.bats" + [ $status -eq 0 ] + [ "${lines[0]}" = "1..3" ] + echo "$output" | grep "^ok . truth" + echo "$output" | grep "^ok . more truth" + echo "$output" | grep "^ok . quasi-truth" +} + +@test "extended syntax in suite" { + FLUNK=1 run bats-exec-suite -x "$FIXTURE_ROOT/multiple/"*.bats + [ $status -eq 1 ] + [ "${lines[0]}" = "1..3" ] + [ "${lines[1]}" = "begin 1 truth" ] + [ "${lines[2]}" = "ok 1 truth" ] + [ "${lines[3]}" = "begin 2 more truth" ] + [ "${lines[4]}" = "ok 2 more truth" ] + [ "${lines[5]}" = "begin 3 quasi-truth" ] + [ "${lines[6]}" = "not ok 3 quasi-truth" ] +} diff --git a/test/test_helper.bash b/test/test_helper.bash new file mode 100644 index 0000000..4ee8410 --- /dev/null +++ b/test/test_helper.bash @@ -0,0 +1,27 @@ +fixtures() { + FIXTURE_ROOT="$BATS_TEST_DIRNAME/fixtures/$1" + bats_trim_filename "$FIXTURE_ROOT" 'RELATIVE_FIXTURE_ROOT' +} + +setup() { + export TMP="$BATS_TEST_DIRNAME/tmp" +} + +filter_control_sequences() { + "$@" | sed $'s,\x1b\\[[0-9;]*[a-zA-Z],,g' +} + +if ! command -v tput >/dev/null; then + tput() { + printf '1000\n' + } + export -f tput +fi + +emit_debug_output() { + printf '%s\n' 'output:' "$output" >&2 +} + +teardown() { + [ -d "$TMP" ] && rm -f "$TMP"/* +} diff --git a/test/tmp/.gitignore b/test/tmp/.gitignore new file mode 100644 index 0000000..241e560 --- /dev/null +++ b/test/tmp/.gitignore @@ -0,0 +1,2 @@ +* + From 9fe3a7324182dc34699ccd405b954f60413e6f91 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Mon, 25 Dec 2017 11:24:14 -0800 Subject: [PATCH 56/58] Add bats to base image and update circleci.yml --- .circleci/config.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ecb2089..f30ef0b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2 jobs: build: docker: - - image: djschaper/circleci:1.1-alpine3.7sc + - image: djschaper/circleci:1.2-alpine3.7scb environment: - CIRCLE_BRANCH: integration/circleci shell: /bin/ash @@ -21,7 +21,10 @@ jobs: command: cp /srv/getsslD/getsslD /bin/ - run: name: Test initial script run - command: getsslD + command: getsslD --help - run: name: Shellcheck getsslD command: shellcheck /srv/getsslD/getsslD + - run: + name: Check for bats executable + command: bats -h From fb0da36495228e85da59fb8612ac6dd4c9ea2160 Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Mon, 25 Dec 2017 11:40:27 -0800 Subject: [PATCH 57/58] Add docker-compose for development environment --- Dockerfile.dev | 8 +++++--- docker-compose.yml | 12 +++--------- test/bats.sh => tests/test.bats | 0 3 files changed, 8 insertions(+), 12 deletions(-) rename test/bats.sh => tests/test.bats (100%) diff --git a/Dockerfile.dev b/Dockerfile.dev index 01a7ace..59a30f6 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -5,11 +5,13 @@ ENV PATH=/bin:$PATH WORKDIR /ssl RUN apk --no-cache --virtual .run-depends add \ + bash \ curl \ drill \ openssl -#COPY getsslD /bin/getsslD +COPY .bats-core/ /opt/bats/ -ENTRYPOINT [ "getsslD" ] -CMD [ "--help" ] +RUN ln -s /opt/bats/libexec/bats /usr/sbin/bats + +ENTRYPOINT ["/bin/ash"] diff --git a/docker-compose.yml b/docker-compose.yml index 69bc65b..4a9dc04 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,15 +4,9 @@ services: development: build: context: . + dockerfile: Dockerfile.dev + working_dir: /opt/src/getssld volumes: - .:/opt/src/getssld - - /tmp/testing:/ssl + - ./tests:/code entrypoint: /bin/ash - - bats: - build: - context: ../../bats-core/ - image: bats:latest - volumes: - - .:/code - command: /code/test/bats.sh diff --git a/test/bats.sh b/tests/test.bats similarity index 100% rename from test/bats.sh rename to tests/test.bats From 3f40f990244d49b912ea2d55f4cdbc421c87218f Mon Sep 17 00:00:00 2001 From: Dan Schaper Date: Mon, 25 Dec 2017 11:59:38 -0800 Subject: [PATCH 58/58] Add comments explaining base image creation and source versions. Signed-off-by: Dan Schaper --- .circleci/config.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index f30ef0b..a942bb8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,6 +2,9 @@ version: 2 jobs: build: docker: + # This image is based on Alpine 3.7, with Shellcheck v0.4.7 and bats-core commit b1da565 (2017-11-13) + # Shellcheck is from base image koalaman/shellcheck-alpine:v0.4.7 + # bats-core is from GitHub repo, see Dockerfile.dev in project root for build environment - image: djschaper/circleci:1.2-alpine3.7scb environment: - CIRCLE_BRANCH: integration/circleci