diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 3b7e4d18..4518c617 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -51,6 +51,7 @@ yamllint --strict "${base_dir}" # https://github.com/hadolint/hadolint#cli hadolint --no-color go/http/Dockerfile +hadolint --no-color go/https/Dockerfile hadolint --no-color js/nodejs/Dockerfile hadolint --no-color rust/http/Dockerfile hadolint --no-color rust/https/Dockerfile diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c0e07aca..20364224 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -57,6 +57,13 @@ jobs: - go/http/go.mod - go/http/go.sum - 'go/http/**.go' + go-https-Dockerfile: + - .hadolint.yaml + - go/https/Dockerfile + go-https: + - go/https/go.mod + - go/https/go.sum + - 'go/https/**.go' rust-http-Dockerfile: - .hadolint.yaml - rust/http/Dockerfile @@ -136,6 +143,35 @@ jobs: name: Build go/http working-directory: go/http run: scripts/build_release.sh + # --- go/https ---------------------------------------------------------- + - if: steps.changes.outputs.go-https-Dockerfile == 'true' + name: Lint go/https/Dockerfile + # https://github.com/hadolint/hadolint-action/releases + uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: go/https/Dockerfile + - if: steps.changes.outputs.go-https == 'true' + name: Install Go for go/https + # https://github.com/actions/setup-go/releases + uses: actions/setup-go@v5.1.0 + with: + go-version-file: go/https/go.mod + - if: steps.changes.outputs.go-https == 'true' + name: Lint go/https files + # https://github.com/golangci/golangci-lint-action/releases + uses: golangci/golangci-lint-action@v6.1.1 + with: + # https://github.com/golangci/golangci-lint/releases + version: v1.62.0 + working-directory: go/https + - if: steps.changes.outputs.go-https == 'true' + name: Test go/https + working-directory: go/https + run: scripts/test.sh + - if: steps.changes.outputs.go-https == 'true' + name: Build go/https + working-directory: go/https + run: scripts/build_release.sh # --- rust/http ------------------------------------------------------ - if: steps.changes.outputs.rust-http-Dockerfile == 'true' name: Lint rust/http Dockerfile diff --git a/README.adoc b/README.adoc index 8984f53d..7ad43cab 100644 --- a/README.adoc +++ b/README.adoc @@ -50,6 +50,7 @@ toc::[] Available Docker health checks: link:go/http/README.adoc[Go - http]:: a Go-based Docker health check for an HTTP URL +link:go/https/README.adoc[Go - https]:: a Go-based Docker health check for an HTTP(S) URL link:js/nodejs/README.adoc/[JavaScript - Node.js]:: a Node.js-based Docker health check for an HTTP(S) URL link:rust/http/README.adoc[Rust - http]:: a Rust-based Docker health check for an HTTP URL link:rust/https/README.adoc[Rust - https]:: a Rust-based Docker health check for an HTTP(S) URL @@ -81,6 +82,10 @@ link:shell/nc/README.adoc[shell - nc]:: an nc-based Docker health check for a da >|5.0M | +|<> +>|5.0M +| + |<> >|8.6M | @@ -123,6 +128,21 @@ $ docker run --rm de.sdavids/sdavids-docker-healthcheck:go-http sh -c 'du -kh /u link:go/http/README.adoc#usage[Go - http] +[#go-https] +==== Go - https + +[source,shell] +---- +$ cd go/https +$ scripts/docker_build.sh -t go-https +$ docker run --rm de.sdavids/sdavids-docker-healthcheck:go-https sh -c 'du -kh /usr/local/bin/healthcheck' +5.0M /usr/local/bin/healthcheck +---- + +===== Usage + +link:go/https/README.adoc#usage[Go - https] + === JavaScript [#js-nodejs] diff --git a/go/https/.dockerignore b/go/https/.dockerignore new file mode 100644 index 00000000..cb0d2c04 --- /dev/null +++ b/go/https/.dockerignore @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: © 2024 Sebastian Davids +# SPDX-License-Identifier: Apache-2.0 + +# https://docs.docker.com/build/building/context/#dockerignore-files + +*~ +*.orig +*.sw[a-p] +*.tmp +.DS_Store +[Dd]esktop.ini +Thumbs.db + +*.adoc +.dockerignore +Dockerfile +scripts/ diff --git a/go/https/Dockerfile b/go/https/Dockerfile new file mode 100644 index 00000000..f3165553 --- /dev/null +++ b/go/https/Dockerfile @@ -0,0 +1,57 @@ +# syntax=docker/dockerfile:1 + +# SPDX-FileCopyrightText: © 2024 Sebastian Davids +# SPDX-License-Identifier: Apache-2.0 + +# https://docs.docker.com/engine/reference/builder/ + +### healthcheck builder ### + +# https://hub.docker.com/_/golang/ +FROM golang:1.23.4-alpine3.20 AS healthcheck + +WORKDIR /app + +COPY go.sum go.mod ./ + +RUN go mod download + +COPY cmd cmd + +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags '-s -w' -o target/healthcheck cmd/healthcheck.go + +LABEL de.sdavids.docker.group="sdavids-docker-healthcheck" \ + de.sdavids.docker.type="builder" + +### HTTPS server ### + +# https://hub.docker.com/_/nginx/ +FROM nginx:1.27.3-alpine3.20-slim AS https_server + +RUN mkdir -p /usr/share/nginx/html/-/health && \ + touch /usr/share/nginx/html/-/health/liveness && \ + printf 'sdavids-docker-healthcheck-go-https

sdavids-docker-healthcheck-go-https

' > /usr/share/nginx/html/index.html && \ + printf 'server {listen 3000 ssl;listen [::]:3000 ssl;ssl_certificate /etc/ssl/certs/server.crt;ssl_certificate_key /etc/ssl/private/server.key;location / {root /usr/share/nginx/html;index index.html;}}' > /etc/nginx/conf.d/default.conf + +LABEL de.sdavids.docker.group="sdavids-docker-healthcheck" \ + de.sdavids.docker.type="builder" + +### final ### + +FROM https_server + +EXPOSE 3000 + +COPY --from=healthcheck \ + /app/target/healthcheck \ + /usr/local/bin/healthcheck + +HEALTHCHECK --interval=5s --timeout=5s --start-period=5s \ + CMD healthcheck || exit 1 + +# https://github.com/opencontainers/image-spec/blob/master/annotations.md +LABEL org.opencontainers.image.licenses="Apache-2.0" \ + org.opencontainers.image.vendor="Sebastian Davids" \ + org.opencontainers.image.title="healthcheck-go-https" \ + de.sdavids.docker.group="sdavids-docker-healthcheck" \ + de.sdavids.docker.type="development" diff --git a/go/https/README.adoc b/go/https/README.adoc new file mode 100644 index 00000000..ed80c733 --- /dev/null +++ b/go/https/README.adoc @@ -0,0 +1,245 @@ +// SPDX-FileCopyrightText: © 2024 Sebastian Davids +// SPDX-License-Identifier: Apache-2.0 += sdavids-docker-healthcheck-go-http +// Metadata: +:description: a Go-based Docker health check for an HTTP(S) URL passed in via ENV. +// Settings: +:sectnums: +:sectanchors: +:sectlinks: +:toc: macro +:toc-placement!: +:source-highlighter: rouge +:rouge-style: github + +ifdef::env-browser[:outfilesuffix: .adoc] + +ifdef::env-github[] +:outfilesuffix: .adoc +:note-caption: :information_source: +:important-caption: :heavy_exclamation_mark: +:tip-caption: :bulb: +endif::[] + +toc::[] + +A Go-based Docker health check for an HTTP(S) URL passed in via ENV. + +[NOTE] +==== +The health check URL has to return https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/200[HTTP 200]. + +The response body is not evaluated. +==== + +[IMPORTANT] +==== +HTTP, HTTP with HTTPS redirect, and HTTPS URLs are supported. +==== + +[TIP] +==== +You can use http://captive.apple.com and https://captive.apple.com for testing. +==== + +This health check uses the HTTP(S) URL passed in via the following ENV variable: + +`HEALTHCHECK_URL`:: the HTTP(S) URL to be used for the health check + +If `HEALTHCHECK_URL` is not set `https://localhost:3000/-/health/liveness` will be used. + +[IMPORTANT] +==== +The health check calls the URL from within the container therefore `localhost` is the running Docker image and not the `localhost` of the Docker host. +==== + +[IMPORTANT] +==== +There is no check whether the given `HEALTHCHECK_URL` is a syntactically correct HTTP(S) URL. +==== + +== Development + +=== Build + +[source,shell] +---- +$ scripts/build.sh +---- + +=> `target/healthcheck` + +=== Test + +[source,shell] +---- +$ scripts/test.sh +---- + +=== Run + +[source,shell] +---- +$ target/healthcheck +$ echo $? +0 + +$ HEALTHCHECK_URL=http://captive.apple.com target/healthcheck +$ echo $? +0 + +$ HEALTHCHECK_URL=https://captive.apple.com target/healthcheck +$ echo $? +0 +---- + +0:: the health check URL returned https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/200[HTTP 200] +69:: the health check URL was unreachable +100:: the health check URL did not return HTTP 200 + +=== Format Source Code + +[source,shell] +---- +$ scripts/format.sh +---- + +=== Lint Source Code + +[source,shell] +---- +$ scripts/lint.sh +---- + +=== Release Build + +[source,shell] +---- +$ scripts/build_release.sh +---- + +=> `target/healthcheck` + +[#usage] +== Usage + +. Copy the health check into your container: ++ +.Dockerfile +[source,dockerfile] +---- +COPY --from=healthcheck \ + /app/target/healthcheck \ + /usr/local/bin/healthcheck +---- + +. Configure the health check: ++ +.Dockerfile +[source,dockerfile] +---- +HEALTHCHECK --interval=5s --timeout=5s --start-period=5s \ + CMD healthcheck || exit 1 +---- ++ +More information: ++ +https://docs.docker.com/engine/reference/builder/#healthcheck[Dockerfile reference - HEALTHCHECK] + +. (Optional) Pass the `HEALTHCHECK_URL` to the `docker container run` invocation: ++ +.scripts/docker_start.sh +[source,dockerfile] +---- +docker container run \ +... + --env HEALTHCHECK_URL='https://localhost:3000/-/health/liveness' \ +... +---- ++ +Alternatively, add the `HEALTHCHECK_URL` to the `Dockerfile`: ++ +.Dockerfile +[source,shell] +---- +ENV HEALTHCHECK_URL="https://localhost:3000/-/health/liveness" +---- + +. (Optional) If you have an `https` healthcheck URL with a custom certificate authority you need to add the certificate authorities root certificate to your image; for example for https://hub.docker.com/_/alpine/[Alpine-based] images: ++ +[source,Dockerfile] +---- +COPY ca.crt /usr/local/share/ca-certificates/ + +# hadolint ignore=DL3018 +RUN apk add --no-cache ca-certificates && \ + update-ca-certificates +---- + +== Example + +link:Dockerfile[Dockerfile]: a simple HTTPS server + +. link:scripts/create_self_signed_cert.sh[Create] a new `localhost` certificate: ++ +[source,shell] +---- +$ scripts/create_self_signed_cert.sh +---- + +. link:scripts/docker_build.sh[Build] the image: ++ +[source,shell] +---- +$ scripts/docker_build.sh +---- + +. link:scripts/docker_start.sh[Start] a container: ++ +[source,shell] +---- +$ scripts/docker_start.sh + +Listen local: https://localhost:3000 + +The URL has been copied to the clipboard. +---- + +. Examine the two endpoints: ++ +[source,shell] +---- +$ curl -s -o /dev/null -w "%{http_code}" https://localhost:3000 +200 +$ curl -s -o /dev/null -w "%{http_code}" https://localhost:3000/-/health/liveness +200 +---- + +. Get the link:scripts/docker_health.sh[health status]: ++ +[source,shell] +---- +$ scripts/docker_health.sh +healthy 0 +---- + +. link:scripts/docker_stop.sh[Stop] the container: ++ +[source,shell] +---- +$ scripts/docker_stop.sh +---- + +. link:scripts/docker_cleanup.sh[Remove all Docker artifacts] related to this project: ++ +[source,shell] +---- +$ scripts/docker_cleanup.sh +---- + +. link:scripts/delete_self_signed_cert.sh[Delete] the `localhost` certificate: ++ +[source,shell] +---- +$ scripts/delete_self_signed_cert.sh +---- diff --git a/go/https/cmd/healthcheck.go b/go/https/cmd/healthcheck.go new file mode 100644 index 00000000..5bf8331e --- /dev/null +++ b/go/https/cmd/healthcheck.go @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: © 2024 Sebastian Davids +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "log/slog" + "net/http" + "os" + "runtime" + "time" + + "github.com/dedelala/sysexits" +) + +func main() { + url, exists := os.LookupEnv("HEALTHCHECK_URL") + if !exists { + url = "https://localhost:3000/-/health/liveness" + } + client := &http.Client{ + Timeout: time.Second * 5, + } + res, err := client.Get(url) //nolint:noctx + if err != nil { + slog.Error("", slog.Any("error", err)) + os.Exit(sysexits.Unavailable) + } + defer func() { + closeErr := res.Body.Close() + if closeErr != nil { + err = closeErr + } + }() + if res.StatusCode == http.StatusOK { + defer os.Exit(sysexits.OK) + } else { + defer os.Exit(100) + } + runtime.Goexit() +} diff --git a/go/https/go.mod b/go/https/go.mod new file mode 100644 index 00000000..1eaaf2c8 --- /dev/null +++ b/go/https/go.mod @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: © 2024 Sebastian Davids +// SPDX-License-Identifier: Apache-2.0 + +// https://go.dev/doc/modules/gomod-ref + +module sdavids.de/sdavids-docker-healthcheck-go-https + +// https://go.dev/doc/devel/release +go 1.23 + +toolchain go1.23.4 + +require github.com/dedelala/sysexits v0.0.0-20170927115716-3d3abae01efc diff --git a/go/https/go.sum b/go/https/go.sum new file mode 100644 index 00000000..07fd6bcd --- /dev/null +++ b/go/https/go.sum @@ -0,0 +1,2 @@ +github.com/dedelala/sysexits v0.0.0-20170927115716-3d3abae01efc h1:nH0/NrIFdL9LpHJTdiNmtLyX4ijCQBhmhy0i7hjUKj8= +github.com/dedelala/sysexits v0.0.0-20170927115716-3d3abae01efc/go.mod h1:jndAi7pgEXpXWQPgK7jP7SGk+JJ7PCngqBAq1emC3PA= diff --git a/go/https/scripts/build.sh b/go/https/scripts/build.sh new file mode 100755 index 00000000..99dd1ca0 --- /dev/null +++ b/go/https/scripts/build.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env sh + +# SPDX-FileCopyrightText: © 2024 Sebastian Davids +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +readonly base_dir="${1:-$PWD}" + +cd "${base_dir}" + +go build -o target/healthcheck cmd/healthcheck.go diff --git a/go/https/scripts/build_release.sh b/go/https/scripts/build_release.sh new file mode 100755 index 00000000..588259bb --- /dev/null +++ b/go/https/scripts/build_release.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env sh + +# SPDX-FileCopyrightText: © 2024 Sebastian Davids +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +readonly base_dir="${1:-$PWD}" + +cd "${base_dir}" + +go build -ldflags '-s -w' -o target/healthcheck cmd/healthcheck.go diff --git a/go/https/scripts/clean.sh b/go/https/scripts/clean.sh new file mode 100755 index 00000000..2270e7fa --- /dev/null +++ b/go/https/scripts/clean.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env sh + +# SPDX-FileCopyrightText: © 2024 Sebastian Davids +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +readonly base_dir="${1:-$PWD}" + +cd "${base_dir}" + +rm -rf target diff --git a/go/https/scripts/create_self_signed_cert.sh b/go/https/scripts/create_self_signed_cert.sh new file mode 100755 index 00000000..924362ff --- /dev/null +++ b/go/https/scripts/create_self_signed_cert.sh @@ -0,0 +1,195 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: © 2024 Sebastian Davids +# SPDX-License-Identifier: Apache-2.0 + +set -Eeu -o pipefail -o posix + +# https://stackoverflow.com/a/3915420 +# https://stackoverflow.com/questions/3915040/how-to-obtain-the-absolute-path-of-a-file-via-shell-bash-zsh-sh#comment100267041_3915420 +command -v realpath >/dev/null 2>&1 || realpath() { + if [ -h "$1" ]; then + # shellcheck disable=SC2012 + ls -ld "$1" | awk '{print $11}' + else + echo "$( + cd "$(dirname -- "$1")" >/dev/null + pwd -P + )/$(basename -- "$1")" + fi +} + +readonly out_dir="${1:-$PWD}" + +if [ -n "${2+x}" ]; then # $2 defined + case $2 in + '' | *[!0-9]*) # $2 is not a positive integer or 0 + echo "'$2' is not a positive integer" >&2 + exit 1 + ;; + *) # $2 is a positive integer or 0 + days="$2" + if [ "${days}" -lt 1 ]; then + echo "'$2' is not a positive integer" >&2 + exit 2 + fi + if [ "${days}" -gt 24855 ]; then + echo "'$2' is too big; range: [1, 24855]" >&2 + exit 3 + fi + if [ "${days}" -gt 180 ]; then + printf "ATTENTION: '%s' exceeds 180 days, the certificate will not be accepted by Apple platforms or Safari; see https://support.apple.com/en-us/103214 for more information.\n\n" "$2" + fi + ;; + esac +else # $2 undefined + days=30 +fi +readonly days + +readonly host_name="${3:-localhost}" + +script_path="$(realpath "$0")" +readonly script_path + +readonly key_path="${out_dir}/key.pem" +readonly cert_path="${out_dir}/cert.pem" + +if [ "$(uname)" = 'Darwin' ]; then + set +e + # https://ss64.com/mac/security-find-cert.html + security find-certificate -c "${host_name}" 1>/dev/null 2>/dev/null + found=$? + set -e + + login_keychain="$(security login-keychain | xargs)" + readonly login_keychain + + if [ "${found}" = 0 ]; then + printf "Keychain %s already has a certificate for '%s'. You can delete the existing certificate via:\n\n\tsecurity delete-certificate -c %s -t %s\n" "${login_keychain}" "${host_name}" "${host_name}" "${login_keychain}" >&2 + exit 4 + fi +fi + +if [ -e "${key_path}" ]; then + printf "The key '%s' already exists.\n" "${key_path}" >&2 + if command -v pbcopy >/dev/null 2>&1; then + printf '%s' "${key_path}" | pbcopy + printf 'The path has been copied to the clipboard.\n' >&2 + elif command -v xclip >/dev/null 2>&1; then + printf '%s' "${key_path}" | xclip -selection clipboard + printf 'The path has been copied to the clipboard.\n' >&2 + elif command -v wl-copy >/dev/null 2>&1; then + printf '%s' "${key_path}" | wl-copy + printf 'The path has been copied to the clipboard.\n' >&2 + fi + exit 5 +fi + +if [ -e "${cert_path}" ]; then + printf "The certificate '%s' already exists.\n" "${cert_path}" >&2 + if command -v pbcopy >/dev/null 2>&1; then + printf '%s' "${cert_path}" | pbcopy + printf 'The path has been copied to the clipboard.\n' >&2 + elif command -v xclip >/dev/null 2>&1; then + printf '%s' "${cert_path}" | xclip -selection clipboard + printf 'The path has been copied to the clipboard.\n' >&2 + elif command -v wl-copy >/dev/null 2>&1; then + printf '%s' "${cert_path}" | wl-copy + printf 'The path has been copied to the clipboard.\n' >&2 + fi + exit 6 +fi + +# https://www.ibm.com/docs/en/ibm-mq/9.3?topic=certificates-distinguished-names +readonly subj="/CN=${host_name}" + +mkdir -p "${out_dir}" + +# https://developer.chrome.com/blog/chrome-58-deprecations/#remove_support_for_commonname_matching_in_certificates +# https://www.openssl.org/docs/manmaster/man5/x509v3_config.html +openssl req \ + -newkey rsa:2048 \ + -x509 \ + -nodes \ + -keyout "${key_path}" \ + -new \ + -out "${cert_path}" \ + -subj "${subj}" \ + -addext "subjectAltName=DNS:${host_name}" \ + -addext 'keyUsage=digitalSignature' \ + -addext 'extendedKeyUsage=serverAuth' \ + -addext "nsComment=This certificate was locally generated by ${script_path}" \ + -sha256 \ + -days "${days}" 2>/dev/null + +chmod 600 "${key_path}" "${cert_path}" + +if [ "$(uname)" = 'Darwin' ]; then + # https://ss64.com/mac/security-cert-verify.html + security verify-cert -q -n -L -r "${cert_path}" + + expires_on="$(date -Idate -v +"${days}"d)" + readonly expires_on + + printf "Adding '%s' certificate (expires on: %s) to keychain %s ...\n" "${host_name}" "${expires_on}" "${login_keychain}" + + # https://ss64.com/mac/security-cert.html + security add-trusted-cert -p ssl -k "${login_keychain}" "${cert_path}" +fi + +( + cd "${out_dir}" + + if [ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" != 'true' ]; then + exit 0 # ${out_dir} not a git repository + fi + + set +e + git check-ignore --quiet key.pem + key_ignored=$? + + git check-ignore --quiet cert.pem + cert_ignored=$? + set -e + + if [ $key_ignored -ne 0 ] || [ $cert_ignored -ne 0 ]; then + printf "\nWARNING: key.pem and/or cert.pem is not ignored in '%s'\n\n" "$PWD/.gitignore" + read -p 'Do you want me to modify your .gitignore file (Y/N)? ' -n 1 -r should_modify + + case "${should_modify}" in + y | Y) printf '\n\n' ;; + *) + printf '\n' + exit 0 + ;; + esac + fi + + if [ $key_ignored -eq 0 ]; then + if [ $cert_ignored -eq 0 ]; then + exit 0 # both already ignored + fi + printf 'cert.pem\n' >>.gitignore + else + if [ $cert_ignored -eq 0 ]; then + printf 'key.pem\n' >>.gitignore + else + printf 'cert.pem\nkey.pem\n' >>.gitignore + fi + fi + + git status +) + +if [ "${host_name}" = 'localhost' ]; then + # https://man.archlinux.org/man/grep.1 + if [ "$(grep -E -i -c '127\.0\.0\.1\s+localhost' /etc/hosts)" -eq 0 ]; then + printf "\nWARNING: /etc/hosts does not have an entry for '127.0.0.1 localhost'\n" >&2 + fi +else + # https://man.archlinux.org/man/grep.1 + if [ "$(grep -E -i -c "127\.0\.0\.1\s+localhost.+${host_name//\./\.}" /etc/hosts)" -eq 0 ]; then + printf "\nWARNING: /etc/hosts does not have an entry for '127.0.0.1 localhost %s'\n" "${host_name}" >&2 + fi +fi diff --git a/go/https/scripts/delete_self_signed_cert.sh b/go/https/scripts/delete_self_signed_cert.sh new file mode 100755 index 00000000..9814fe58 --- /dev/null +++ b/go/https/scripts/delete_self_signed_cert.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env sh + +# SPDX-FileCopyrightText: © 2024 Sebastian Davids +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +readonly base_dir="${1:-$PWD}" + +readonly host_name="${2:-localhost}" + +readonly key_path="${base_dir}/key.pem" +readonly cert_path="${base_dir}/cert.pem" + +if [ "$(uname)" = 'Darwin' ]; then + set +e + # https://ss64.com/mac/security-find-cert.html + security find-certificate -c "${host_name}" 1>/dev/null 2>/dev/null + found=$? + set -e + + if [ "${found}" = 0 ]; then + login_keychain="$(security login-keychain | xargs)" + readonly login_keychain + + echo "Removing '${host_name}' certificate from keychain ${login_keychain} ..." + + # https://ss64.com/mac/security-delete-cert.html + security delete-certificate -c "${host_name}" -t "${login_keychain}" + fi +fi + +if [ -f "${key_path}" ]; then + rm -f "${key_path}" +fi + +if [ -f "${cert_path}" ]; then + rm -f "${cert_path}" +fi + +# delete empty certs dir if not $PWD +if [ -d "${base_dir}" ] \ + && [ "${base_dir}" != "$PWD" ] \ + && [ "${base_dir}" != '.' ] \ + && [ -z "$(ls -A "${base_dir}")" ]; then + + rmdir "${base_dir}" +fi diff --git a/go/https/scripts/docker_build.sh b/go/https/scripts/docker_build.sh new file mode 100755 index 00000000..d21e1e54 --- /dev/null +++ b/go/https/scripts/docker_build.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: © 2024 Sebastian Davids +# SPDX-License-Identifier: Apache-2.0 + +# script needs to be invoked from the go/http directory + +set -Eeu -o pipefail -o posix + +while getopts ':d:nt:' opt; do + case "${opt}" in + d) + dockerfile="${OPTARG}" + ;; + n) + no_cache='--pull --no-cache' + ;; + t) + tag="${OPTARG}" + ;; + ?) + echo "Usage: $0 [-d Dockerfile] [-n] [-t tag]" >&2 + exit 1 + ;; + esac +done + +readonly dockerfile="${dockerfile:-$PWD/Dockerfile}" + +readonly no_cache="${no_cache:-}" + +readonly tag="${tag:-local}" + +if [ ! -f "${dockerfile}" ]; then + echo "Dockerfile '${dockerfile}' does not exist" >&2 + exit 2 +fi + +# https://docs.docker.com/reference/cli/docker/image/tag/#description +readonly namespace='de.sdavids' +readonly repository='sdavids-docker-healthcheck' + +readonly label_group='de.sdavids.docker.group' + +readonly image_name="${namespace}/${repository}" + +# https://reproducible-builds.org/docs/source-date-epoch/ +if [ -z "${SOURCE_DATE_EPOCH+x}" ]; then + if [ -z "$(git status --porcelain=v1 2>/dev/null)" ]; then + SOURCE_DATE_EPOCH="$(git log --max-count=1 --pretty=format:%ct)" + else + SOURCE_DATE_EPOCH="$(date +%s)" + fi + export SOURCE_DATE_EPOCH +fi + +if [ "$(uname)" = 'Darwin' ]; then + created_at="$(date -r "${SOURCE_DATE_EPOCH}" -Iseconds -u | sed -e 's/+00:00$/Z/')" +else + created_at="$(date -d "@${SOURCE_DATE_EPOCH}" -Iseconds -u | sed -e 's/+00:00$/Z/')" +fi +readonly created_at + +if [ -n "${GITHUB_SHA+x}" ]; then + # https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables + commit="${GITHUB_SHA}" +elif [ "$(git rev-parse --is-inside-work-tree 2>/dev/null)" != 'true' ]; then + commit='N/A' +else + if [ -z "$(git status --porcelain=v1 2>/dev/null)" ]; then + ext='' + else + ext='-next' + fi + commit="$(git rev-parse --verify HEAD)${ext}" + unset ext +fi +readonly commit + +# https://github.com/opencontainers/image-spec/blob/master/annotations.md +# shellcheck disable=SC2086 +docker image build \ + ${no_cache} \ + --file "${dockerfile}" \ + --compress \ + --tag "${image_name}:latest" \ + --tag "${image_name}:${tag}" \ + --label "${label_group}=${repository}" \ + --label "org.opencontainers.image.revision=${commit}" \ + --label "org.opencontainers.image.created=${created_at}" \ + . + +echo + +docker image inspect -f '{{json .Config.Labels}}' "${image_name}:${tag}" diff --git a/go/https/scripts/docker_cleanup.sh b/go/https/scripts/docker_cleanup.sh new file mode 100755 index 00000000..d23595fd --- /dev/null +++ b/go/https/scripts/docker_cleanup.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env sh + +# SPDX-FileCopyrightText: © 2024 Sebastian Davids +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +readonly repository='sdavids-docker-healthcheck' + +readonly label_group='de.sdavids.docker.group' + +readonly label="${label_group}=${repository}" + +docker container prune --force --filter="label=${label}" + +docker volume prune --force --filter="label=${label}" + +docker image prune --force --filter="label=${label}" --all + +docker network prune --force --filter="label=${label}" diff --git a/go/https/scripts/docker_health.sh b/go/https/scripts/docker_health.sh new file mode 100755 index 00000000..fdc5228c --- /dev/null +++ b/go/https/scripts/docker_health.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env sh + +# SPDX-FileCopyrightText: © 2024 Sebastian Davids +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +readonly container_name='sdavids-docker-healthcheck-go-https' + +container_id="$(docker container ls --all --quiet --filter="name=^/${container_name}$")" + +if [ -n "${container_id}" ]; then + # HEALTHCHECK defined? + if [ "$(docker container inspect --format='{{.State.Health}}' "${container_name}")" = '' ]; then + exit + fi + + docker container inspect \ + --format='{{.State.Health.Status}} {{.State.Health.FailingStreak}}' \ + "${container_name}" +fi diff --git a/go/https/scripts/docker_logs.sh b/go/https/scripts/docker_logs.sh new file mode 100755 index 00000000..95b77cb4 --- /dev/null +++ b/go/https/scripts/docker_logs.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env sh + +# SPDX-FileCopyrightText: © 2024 Sebastian Davids +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +readonly container_name='sdavids-docker-healthcheck-go-https' + +docker container logs \ + --follow \ + --timestamps \ + "${container_name}" diff --git a/go/https/scripts/docker_remove.sh b/go/https/scripts/docker_remove.sh new file mode 100755 index 00000000..49788c11 --- /dev/null +++ b/go/https/scripts/docker_remove.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env sh + +# SPDX-FileCopyrightText: © 2024 Sebastian Davids +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +readonly container_name='sdavids-docker-healthcheck-go-https' + +if [ -n "$(docker container ls --all --quiet --filter="name=^/${container_name}$")" ]; then + docker container stop "${container_name}" +fi + +# container not started with --rm ? +if [ -n "$(docker container ls --all --quiet --filter="name=^/${container_name}$")" ]; then + docker container remove --force --volumes "${container_name}" +fi diff --git a/go/https/scripts/docker_sh.sh b/go/https/scripts/docker_sh.sh new file mode 100755 index 00000000..9a2e1579 --- /dev/null +++ b/go/https/scripts/docker_sh.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env sh + +# SPDX-FileCopyrightText: © 2024 Sebastian Davids +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +readonly container_name='sdavids-docker-healthcheck-go-https' + +docker exec \ + --interactive \ + --tty \ + "${container_name}" \ + sh diff --git a/go/https/scripts/docker_start.sh b/go/https/scripts/docker_start.sh new file mode 100755 index 00000000..567e31df --- /dev/null +++ b/go/https/scripts/docker_start.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env sh + +# SPDX-FileCopyrightText: © 2024 Sebastian Davids +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +readonly https_port="${1:-3000}" + +readonly tag='local' + +# https://docs.docker.com/reference/cli/docker/image/tag/#description +readonly namespace='de.sdavids' +readonly repository='sdavids-docker-healthcheck' + +readonly label_group='de.sdavids.docker.group' + +readonly label="${label_group}=${repository}" + +readonly image_name="${namespace}/${repository}" + +readonly container_name='sdavids-docker-healthcheck-go-https' + +readonly host_name='localhost' + +readonly network_name="${repository}" + +docker network inspect "${network_name}" >/dev/null 2>&1 \ + || docker network create \ + --driver bridge "${network_name}" \ + --label "${label_group}=${namespace}" >/dev/null + +# to ensure ${label} is set, we use --label "${label}" +# which might overwrite the label ${label_group} of the image +docker container run \ + --init \ + --rm \ + --detach \ + --security-opt='no-new-privileges=true' \ + --cap-add=chown \ + --cap-add=setgid \ + --cap-add=setuid \ + --cap-drop=all \ + --network="${network_name}" \ + --publish "${https_port}:3000" \ + --volume "$PWD/cert.pem:/etc/ssl/certs/server.crt:ro" \ + --volume "$PWD/key.pem:/etc/ssl/private/server.key:ro" \ + --name "${container_name}" \ + --label "${label}" \ + "${image_name}:${tag}" >/dev/null + +readonly url="https://${host_name}:${https_port}" + +printf '\nListen local: %s\n' "${url}" + +if command -v pbcopy >/dev/null 2>&1; then + printf '%s' "${url}" | pbcopy + printf '\nThe URL has been copied to the clipboard.\n' +elif command -v xclip >/dev/null 2>&1; then + printf '%s' "${url}" | xclip -selection clipboard + printf '\nThe URL has been copied to the clipboard.\n' +elif command -v wl-copy >/dev/null 2>&1; then + printf '%s' "${url}" | wl-copy + printf '\nThe URL has been copied to the clipboard.\n' +fi diff --git a/go/https/scripts/docker_stop.sh b/go/https/scripts/docker_stop.sh new file mode 100755 index 00000000..675bba20 --- /dev/null +++ b/go/https/scripts/docker_stop.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env sh + +# SPDX-FileCopyrightText: © 2024 Sebastian Davids +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +readonly container_name='sdavids-docker-healthcheck-go-https' + +container_id="$(docker container ls --all --quiet --filter="name=^/${container_name}$")" +readonly container_id + +if [ -n "${container_id}" ]; then + docker stop "${container_id}" >/dev/null +fi + +readonly network_name='sdavids-docker-healthcheck' + +if docker network inspect "${network_name}" >/dev/null 2>&1; then + docker network rm "${network_name}" >/dev/null +fi diff --git a/go/https/scripts/format.sh b/go/https/scripts/format.sh new file mode 100755 index 00000000..d5dd59f3 --- /dev/null +++ b/go/https/scripts/format.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env sh + +# SPDX-FileCopyrightText: © 2024 Sebastian Davids +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +readonly base_dir="${1:-$PWD}" + +cd "${base_dir}" + +gofmt -s -w . diff --git a/go/https/scripts/format_check.sh b/go/https/scripts/format_check.sh new file mode 100755 index 00000000..4ee1bc39 --- /dev/null +++ b/go/https/scripts/format_check.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env sh + +# SPDX-FileCopyrightText: © 2024 Sebastian Davids +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +readonly base_dir="${1:-$PWD}" + +cd "${base_dir}" diff --git a/go/https/scripts/lint.sh b/go/https/scripts/lint.sh new file mode 100755 index 00000000..ed27bfdf --- /dev/null +++ b/go/https/scripts/lint.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env sh + +# SPDX-FileCopyrightText: © 2024 Sebastian Davids +# SPDX-License-Identifier: Apache-2.0 + +# golangci-lint needs to be in $PATH +# +# https://golangci-lint.run/usage/quick-start +# https://github.com/golangci/golangci-lint/issues/2654#issuecomment-1606439587 + +set -eu + +readonly base_dir="${1:-$PWD}" + +cd "${base_dir}" + +go list -f '{{.Dir}}/...' -m | xargs golangci-lint run diff --git a/go/https/scripts/lint_fix.sh b/go/https/scripts/lint_fix.sh new file mode 100755 index 00000000..180543fe --- /dev/null +++ b/go/https/scripts/lint_fix.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env sh + +# SPDX-FileCopyrightText: © 2024 Sebastian Davids +# SPDX-License-Identifier: Apache-2.0 + +# golangci-lint needs to be in $PATH +# +# https://golangci-lint.run/usage/quick-start +# https://github.com/golangci/golangci-lint/issues/2654#issuecomment-1606439587 + +set -eu + +readonly base_dir="${1:-$PWD}" + +cd "${base_dir}" + +go list -f '{{.Dir}}/...' -m | xargs golangci-lint run --fix diff --git a/go/https/scripts/renew_self_signed_cert.sh b/go/https/scripts/renew_self_signed_cert.sh new file mode 100755 index 00000000..6b818887 --- /dev/null +++ b/go/https/scripts/renew_self_signed_cert.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env sh + +# SPDX-FileCopyrightText: © 2024 Sebastian Davids +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +readonly base_dir="${1:-$PWD}" + +readonly key_path="${base_dir}/key.pem" +readonly cert_path="${base_dir}/cert.pem" + +if [ ! -f "${key_path}" ]; then + echo "key '${key_path}' does not exist" >&2 + exit 11 +fi + +if [ ! -f "${cert_path}" ]; then + echo "cert '${cert_path}' does not exist" >&2 + exit 12 +fi + +# https://stackoverflow.com/a/3915420 +# https://stackoverflow.com/questions/3915040/how-to-obtain-the-absolute-path-of-a-file-via-shell-bash-zsh-sh#comment100267041_3915420 +command -v realpath >/dev/null 2>&1 || realpath() { + if [ -h "$1" ]; then + # shellcheck disable=SC2012 + ls -ld "$1" | awk '{print $11}' + else + echo "$( + cd "$(dirname -- "$1")" >/dev/null + pwd -P + )/$(basename -- "$1")" + fi +} + +script_path="$(realpath "$0")" +readonly script_path + +script_base_dir="$(dirname -- "${script_path}")" +readonly script_base_dir + +readonly create_script_path="${script_base_dir}/create_self_signed_cert.sh" +readonly delete_script_path="${script_base_dir}/delete_self_signed_cert.sh" + +if [ ! -f "${create_script_path}" ]; then + echo "create script '${create_script_path}' does not exist" >&2 + exit 13 +fi + +if [ ! -f "${delete_script_path}" ]; then + echo "delete script '${delete_script_path}' does not exist" >&2 + exit 14 +fi + +if [ -n "${1+x}" ]; then + if [ -n "${2+x}" ]; then + if [ -n "${3+x}" ]; then + $delete_script_path "$1" "$3" + $create_script_path "$1" "$2" "$3" + else + $delete_script_path "$1" + $create_script_path "$1" "$2" + fi + else + $delete_script_path "$1" + $create_script_path "$1" + fi +else + $delete_script_path + $create_script_path +fi diff --git a/go/https/scripts/test.sh b/go/https/scripts/test.sh new file mode 100755 index 00000000..4ee1bc39 --- /dev/null +++ b/go/https/scripts/test.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env sh + +# SPDX-FileCopyrightText: © 2024 Sebastian Davids +# SPDX-License-Identifier: Apache-2.0 + +set -eu + +readonly base_dir="${1:-$PWD}" + +cd "${base_dir}" diff --git a/go/https/scripts/verify_self_signed_cert.sh b/go/https/scripts/verify_self_signed_cert.sh new file mode 100755 index 00000000..77bcdb8c --- /dev/null +++ b/go/https/scripts/verify_self_signed_cert.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash + +# SPDX-FileCopyrightText: © 2024 Sebastian Davids +# SPDX-License-Identifier: Apache-2.0 + +set -Eeu -o pipefail -o posix + +readonly base_dir="${1:-$PWD}" + +readonly cert_path="${base_dir}/cert.pem" +readonly key_path="${base_dir}/key.pem" + +if [ "$(uname)" = 'Darwin' ]; then + if [ "$(stat -f '%A' "${cert_path}")" != '600' ]; then + printf '\nWARNING: cert.pem does not have the correct permissions. You can change them via:\n\n\tchmod 600 %s\n' "${cert_path}" + exit 1 + fi + + if [ "$(stat -f '%A' "${key_path}")" != '600' ]; then + printf '\nWARNING: key.pem does not have the correct permissions. You can change them via:\n\n\tchmod 600 %s\n' "${key_path}" + exit 2 + fi +else + if [ "$(stat -c '%a' "${cert_path}")" != '600' ]; then + printf '\nWARNING: cert.pem does not have the correct permissions. You can change them via:\n\n\tchmod 600 %s\n' "${cert_path}" + exit 3 + fi + + if [ "$(stat -c '%a' "${key_path}")" != '600' ]; then + printf '\nWARNING: key.pem does not have the correct permissions. You can change them via:\n\n\tchmod 600 %s\n' "${key_path}" + exit 4 + fi +fi + +host_name="$(openssl x509 -ext subjectAltName -noout -in cert.pem | grep 'DNS:' | sed 's/DNS:\(.*\)/\1/' | awk '{$1=$1};1')" +readonly host_name + +if [ "$(uname)" = 'Darwin' ]; then + # https://ss64.com/mac/security-cert-verify.html + security verify-cert -q -n -L -r "${cert_path}" + + # https://ss64.com/mac/security-find-cert.html + security find-certificate -c "${host_name}" +fi + +printf '\n%s\n' "${cert_path}" + +openssl x509 -text -noout -in "${cert_path}" + +if [ "${host_name}" = 'localhost' ]; then + # https://man.archlinux.org/man/grep.1 + if [ "$(grep -E -i -c '127\.0\.0\.1\s+localhost' /etc/hosts)" -eq 0 ]; then + echo "WARNING: /etc/hosts does not have an entry for '127.0.0.1 localhost'" >&2 + fi +else + # https://man.archlinux.org/man/grep.1 + if [ "$(grep -E -i -c "127\.0\.0\.1\s+localhost.+${host_name//\./\.}" /etc/hosts)" -eq 0 ]; then + echo "WARNING: /etc/hosts does not have an entry for '127.0.0.1 localhost ${host_name}'" >&2 + fi +fi diff --git a/scripts/format.sh b/scripts/format.sh index 50683afc..8032af79 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -10,6 +10,7 @@ set -eu readonly base_dir="$PWD" go/http/scripts/format.sh "${base_dir}/go/http" +go/https/scripts/format.sh "${base_dir}/go/https" js/nodejs/scripts/format.sh "${base_dir}/js/nodejs" rust/http/scripts/format.sh "${base_dir}/rust/http" rust/https/scripts/format.sh "${base_dir}/rust/https" diff --git a/scripts/format_check.sh b/scripts/format_check.sh index edc1b17c..afb8555f 100755 --- a/scripts/format_check.sh +++ b/scripts/format_check.sh @@ -10,6 +10,7 @@ set -eu readonly base_dir="$PWD" go/http/scripts/format_check.sh "${base_dir}/go/http" +go/https/scripts/format_check.sh "${base_dir}/go/https" js/nodejs/scripts/format_check.sh "${base_dir}/js/nodejs" rust/http/scripts/format_check.sh "${base_dir}/rust/http" rust/https/scripts/format_check.sh "${base_dir}/rust/https" diff --git a/scripts/lint.sh b/scripts/lint.sh index 507268b8..720cca18 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -10,6 +10,7 @@ set -eu readonly base_dir="$PWD" go/http/scripts/lint.sh "${base_dir}/go/http" +go/https/scripts/lint.sh "${base_dir}/go/https" js/nodejs/scripts/lint.sh "${base_dir}/js/nodejs" rust/http/scripts/lint.sh "${base_dir}/rust/http" rust/https/scripts/lint.sh "${base_dir}/rust/https"