From 6e5afb84a99e2b451f5f0f05ecf901277ab827a7 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Tue, 7 Jul 2026 06:56:51 -0400 Subject: [PATCH] feat: port certs :report subcommand to golang The bash :report implementation for the certs plugin is replaced with a compiled golang binary. The certificate inspection getters run openssl and reproduce the existing field extraction, so the ssl report keys are unchanged, while collection now happens in parallel and json is marshalled directly. The now-unused fn-ssl-* display helpers are dropped, and fn-certs-set and fn-certs-remove remain for certs:add and certs:remove. --- go.work | 1 + plugins/certs/.gitignore | 4 + plugins/certs/Makefile | 13 ++ plugins/certs/go.mod | 29 +++ plugins/certs/go.sum | 55 +++++ plugins/certs/internal-functions | 162 ------------- plugins/certs/report | 6 - plugins/certs/report.go | 233 +++++++++++++++++++ plugins/certs/src/subcommands/subcommands.go | 40 ++++ plugins/certs/src/triggers/triggers.go | 31 +++ plugins/certs/subcommands.go | 29 +++ plugins/certs/subcommands/report | 6 - 12 files changed, 435 insertions(+), 174 deletions(-) create mode 100644 plugins/certs/.gitignore create mode 100644 plugins/certs/Makefile create mode 100644 plugins/certs/go.mod create mode 100644 plugins/certs/go.sum mode change 100755 => 100644 plugins/certs/internal-functions delete mode 100755 plugins/certs/report create mode 100644 plugins/certs/report.go create mode 100644 plugins/certs/src/subcommands/subcommands.go create mode 100644 plugins/certs/src/triggers/triggers.go create mode 100644 plugins/certs/subcommands.go delete mode 100755 plugins/certs/subcommands/report diff --git a/go.work b/go.work index afe21daad..2aa88dabc 100644 --- a/go.work +++ b/go.work @@ -12,6 +12,7 @@ use ( ./plugins/builder-railpack ./plugins/buildpacks ./plugins/caddy-vhosts + ./plugins/certs ./plugins/checks ./plugins/common ./plugins/config diff --git a/plugins/certs/.gitignore b/plugins/certs/.gitignore new file mode 100644 index 000000000..dac9d7731 --- /dev/null +++ b/plugins/certs/.gitignore @@ -0,0 +1,4 @@ +/report +/report-subcommand +/triggers +/subcommands/report diff --git a/plugins/certs/Makefile b/plugins/certs/Makefile new file mode 100644 index 000000000..92ef9be66 --- /dev/null +++ b/plugins/certs/Makefile @@ -0,0 +1,13 @@ +GOARCH ?= amd64 +TRIGGERS = triggers/report +BUILD = report-subcommand triggers +PLUGIN_NAME = certs + +clean-report-subcommand: + rm -rf report-subcommand + +report-subcommand: clean-report-subcommand src/subcommands/subcommands.go + GOARCH=$(GOARCH) go build -mod=readonly -ldflags="-s -w" $(GO_ARGS) -o report-subcommand src/subcommands/subcommands.go + ln -sf ../report-subcommand subcommands/report + +include ../../common.mk diff --git a/plugins/certs/go.mod b/plugins/certs/go.mod new file mode 100644 index 000000000..28a0fda20 --- /dev/null +++ b/plugins/certs/go.mod @@ -0,0 +1,29 @@ +module github.com/dokku/dokku/plugins/certs + +go 1.26.2 + +require ( + github.com/dokku/dokku/plugins/common v0.0.0-00010101000000-000000000000 + github.com/spf13/pflag v1.0.10 +) + +require ( + github.com/alexellis/go-execute/v2 v2.2.1 // indirect + github.com/fatih/color v1.19.0 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/kr/fs v0.1.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/melbahja/goph v1.5.1 // indirect + github.com/otiai10/copy v1.14.1 // indirect + github.com/otiai10/mint v1.6.3 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/sftp v1.13.10 // indirect + github.com/ryanuber/columnize v2.1.2+incompatible // indirect + golang.org/x/crypto v0.53.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect +) + +replace github.com/dokku/dokku/plugins/common => ../common diff --git a/plugins/certs/go.sum b/plugins/certs/go.sum new file mode 100644 index 000000000..5a5292407 --- /dev/null +++ b/plugins/certs/go.sum @@ -0,0 +1,55 @@ +github.com/alexellis/go-execute/v2 v2.2.1 h1:4Ye3jiCKQarstODOEmqDSRCqxMHLkC92Bhse743RdOI= +github.com/alexellis/go-execute/v2 v2.2.1/go.mod h1:FMdRnUTiFAmYXcv23txrp3VYZfLo24nMpiIneWgKHTQ= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/melbahja/goph v1.5.1 h1:gdZyjn5nM6FO6mAZrv7XqE2gArQbb+V6FdIyH3Ru3vA= +github.com/melbahja/goph v1.5.1/go.mod h1:dDwo+44cmvfDLdiVpc6fJxexf5BA5yEDUeE5YgtuDO4= +github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I= +github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= +github.com/otiai10/copy v1.14.1 h1:5/7E6qsUMBaH5AnQ0sSLzzTg1oTECmcCmT6lvF45Na8= +github.com/otiai10/copy v1.14.1/go.mod h1:oQwrEDDOci3IM8dJF0d8+jnbfPDllW6vUjNc3DoZm9I= +github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= +github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU= +github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/ryanuber/columnize v2.1.2+incompatible h1:C89EOx/XBWwIXl8wm8OPJBd7kPF25UfsK2X7Ph/zCAk= +github.com/ryanuber/columnize v2.1.2+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= +golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= +golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/plugins/certs/internal-functions b/plugins/certs/internal-functions old mode 100755 new mode 100644 index 2686c0a0c..be7f27578 --- a/plugins/certs/internal-functions +++ b/plugins/certs/internal-functions @@ -46,165 +46,3 @@ fn-certs-remove() { plugn trigger post-domains-update "$APP" } -cmd-certs-report() { - declare desc="displays an ssl report for one or more apps" - declare cmd="certs:report" - [[ "$1" == "$cmd" ]] && shift 1 - fn-report-parse-args "$@" - set -- "${REPORT_ARGS[@]}" - declare APP="${1:-}" INFO_FLAG="${2:-}" - - if [[ -n "$APP" ]] && [[ "$APP" == --* ]]; then - INFO_FLAG="$APP" - APP="" - fi - - if [[ "$REPORT_IS_GLOBAL" == "true" ]]; then - APP="--global" - fi - - if [[ -z "$APP" ]] && [[ -z "$INFO_FLAG" ]]; then - INFO_FLAG="true" - fi - - if [[ "$APP" == "--global" ]]; then - cmd-certs-report-single "$APP" "$INFO_FLAG" "$REPORT_FORMAT" - elif [[ -z "$APP" ]]; then - for app in $(dokku_apps); do - cmd-certs-report-single "$app" "$INFO_FLAG" "$REPORT_FORMAT" | tee || true - done - else - cmd-certs-report-single "$APP" "$INFO_FLAG" "$REPORT_FORMAT" - fi -} - -cmd-certs-report-single() { - declare APP="$1" INFO_FLAG="$2" FORMAT="${3:-stdout}" - if [[ "$INFO_FLAG" == "true" ]]; then - INFO_FLAG="" - fi - local flag_map=() - if [[ "$APP" != "--global" ]]; then - verify_app_name "$APP" - flag_map=( - "--ssl-dir: $DOKKU_ROOT/$APP/tls" - "--ssl-enabled: $(fn-ssl-enabled "$APP")" - "--ssl-hostnames: $(fn-ssl-hostnames "$APP")" - "--ssl-expires-at: $(fn-ssl-expires-at "$APP")" - "--ssl-issuer: $(fn-ssl-issuer "$APP")" - "--ssl-starts-at: $(fn-ssl-starts-at "$APP")" - "--ssl-subject: $(fn-ssl-subject "$APP")" - "--ssl-verified: $(fn-ssl-verified "$APP")" - ) - fi - - fn-report-validate-format "$FORMAT" "$INFO_FLAG" - - if [[ "$FORMAT" == "json" ]]; then - fn-report-emit-json flag_map "ssl" - return - fi - - if [[ -z "$INFO_FLAG" ]]; then - if [[ "$APP" == "--global" ]]; then - dokku_log_info2_quiet "global ssl information" - else - dokku_log_info2_quiet "$APP ssl information" - fi - for flag in "${flag_map[@]}"; do - key="$(echo "${flag#--}" | cut -f1 -d' ' | tr - ' ')" - dokku_log_verbose "$(printf "%-30s %-25s" "${key^}" "${flag#*: }")" - done - else - local match=false - for flag in "${flag_map[@]}"; do - valid_flags="${valid_flags} $(echo "$flag" | cut -d':' -f1)" - if [[ "$flag" == "${INFO_FLAG}:"* ]]; then - value=${flag#*: } - size="${#value}" - if [[ "$size" -ne 0 ]]; then - echo "$value" && match=true - else - match=true - fi - fi - done - [[ "$match" == "true" ]] || dokku_log_fail "Invalid flag passed, valid flags:${valid_flags}" - fi -} - -fn-ssl-enabled() { - declare APP="$1" - local SSL_ENABLED=false - - if is_ssl_enabled "$APP"; then - SSL_ENABLED=true - fi - echo "$SSL_ENABLED" -} - -fn-ssl-expires-at() { - declare APP="$1" - local APP_SSL_PATH="$DOKKU_ROOT/$APP/tls" - - if is_ssl_enabled "$APP"; then - openssl x509 -in "$APP_SSL_PATH/server.crt" -noout -text | grep "Not After :" | awk -F " : " '{ print $2 }' - fi -} - -fn-ssl-hostnames() { - declare APP="$1" - - if is_ssl_enabled "$APP"; then - get_ssl_hostnames "$APP" | xargs - fi -} - -fn-ssl-issuer() { - declare APP="$1" - local APP_SSL_PATH="$DOKKU_ROOT/$APP/tls" - - if is_ssl_enabled "$APP"; then - openssl x509 -in "$APP_SSL_PATH/server.crt" -noout -text | grep "Issuer:" | head -n1 | sed -e 's/Issuer: //g' -e 's/^[[:space:]]*//' - fi -} - -fn-ssl-starts-at() { - declare APP="$1" - local APP_SSL_PATH="$DOKKU_ROOT/$APP/tls" - - if is_ssl_enabled "$APP"; then - openssl x509 -in "$APP_SSL_PATH/server.crt" -noout -text | grep "Not Before:" | awk -F ": " '{ print $2 }' - fi -} - -fn-ssl-subject() { - declare APP="$1" - local APP_SSL_PATH="$DOKKU_ROOT/$APP/tls" - - if is_ssl_enabled "$APP"; then - openssl x509 -in "$APP_SSL_PATH/server.crt" -noout -subject | sed -e "s:subject= ::g" | sed -e "s:^/::g" | sed -e "s:/:; :g" - fi -} - -fn-ssl-verified() { - declare APP="$1" - local APP_SSL_PATH="$DOKKU_ROOT/$APP/tls" - local SSL_VERIFY_OUTPUT=false SSL_SELF_SIGNED="self signed" - - if ! is_ssl_enabled "$APP"; then - return - fi - - if [[ -f "$APP_SSL_PATH/server.letsencrypt.crt" ]]; then - SSL_VERIFY_OUTPUT="$(openssl verify -verbose -purpose sslserver -CAfile "$APP_SSL_PATH/server.crt" "$APP_SSL_PATH/server.letsencrypt.crt" 2>/dev/null | awk -F ':' '{ print $2 }' | tail -1 | xargs || true)" - else - SSL_VERIFY_OUTPUT="$(openssl verify -verbose -purpose sslserver "$APP_SSL_PATH/server.crt" 2>/dev/null | awk -F ':' '{ print $2 }' | tail -1 | xargs || true)" - fi - - if [[ "$SSL_VERIFY_OUTPUT" == "OK" ]]; then - SSL_SELF_SIGNED="verified by a certificate authority" - fi - - echo "$SSL_SELF_SIGNED" -} diff --git a/plugins/certs/report b/plugins/certs/report deleted file mode 100755 index b323feb51..000000000 --- a/plugins/certs/report +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -source "$PLUGIN_AVAILABLE_PATH/certs/internal-functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-certs-report-single "$@" diff --git a/plugins/certs/report.go b/plugins/certs/report.go new file mode 100644 index 000000000..02ee5ec15 --- /dev/null +++ b/plugins/certs/report.go @@ -0,0 +1,233 @@ +package certs + +import ( + "os" + "path/filepath" + "regexp" + "sort" + "strings" + + "github.com/dokku/dokku/plugins/common" +) + +var dnsPrefixRegex = regexp.MustCompile(`[[:space:]]*DNS:`) + +// ReportSingleApp is an internal function that displays the ssl report for one or more apps +func ReportSingleApp(appName string, format string, infoFlag string) error { + flags := map[string]common.ReportFunc{} + if appName != "--global" { + if err := common.VerifyAppName(appName); err != nil { + return err + } + flags = map[string]common.ReportFunc{ + "--ssl-dir": reportSSLDir, + "--ssl-enabled": reportSSLEnabled, + "--ssl-hostnames": reportSSLHostnames, + "--ssl-expires-at": reportSSLExpiresAt, + "--ssl-issuer": reportSSLIssuer, + "--ssl-starts-at": reportSSLStartsAt, + "--ssl-subject": reportSSLSubject, + "--ssl-verified": reportSSLVerified, + } + } + + flagKeys := []string{} + for flagKey := range flags { + flagKeys = append(flagKeys, flagKey) + } + + infoFlags := common.CollectReport(appName, infoFlag, flags) + return common.ReportSingleApp(common.ReportSingleAppInput{ + ReportType: "ssl", + AppName: appName, + InfoFlag: infoFlag, + InfoFlags: infoFlags, + InfoFlagKeys: flagKeys, + Format: format, + TrimPrefix: true, + UppercaseFirstCharacter: true, + EmitLegacyPrefix: false, + }) +} + +func certTLSPath(appName string) string { + return filepath.Join(common.MustGetEnv("DOKKU_ROOT"), appName, "tls") +} + +func isSSLEnabled(appName string) bool { + tlsPath := certTLSPath(appName) + if _, err := os.Stat(filepath.Join(tlsPath, "server.crt")); err != nil { + return false + } + if _, err := os.Stat(filepath.Join(tlsPath, "server.key")); err != nil { + return false + } + + return true +} + +func opensslCertText(appName string) string { + result, err := common.CallExecCommand(common.ExecCommandInput{ + Command: "openssl", + Args: []string{"x509", "-in", filepath.Join(certTLSPath(appName), "server.crt"), "-noout", "-text"}, + }) + if err != nil { + return "" + } + + return result.Stdout +} + +func reportSSLDir(appName string) string { + return certTLSPath(appName) +} + +func reportSSLEnabled(appName string) string { + if isSSLEnabled(appName) { + return "true" + } + + return "false" +} + +func reportSSLExpiresAt(appName string) string { + if !isSSLEnabled(appName) { + return "" + } + + for _, line := range strings.Split(opensslCertText(appName), "\n") { + if strings.Contains(line, "Not After :") { + parts := strings.Split(line, " : ") + if len(parts) >= 2 { + return parts[1] + } + } + } + + return "" +} + +func reportSSLStartsAt(appName string) string { + if !isSSLEnabled(appName) { + return "" + } + + for _, line := range strings.Split(opensslCertText(appName), "\n") { + if strings.Contains(line, "Not Before:") { + parts := strings.Split(line, ": ") + if len(parts) >= 2 { + return parts[1] + } + } + } + + return "" +} + +func reportSSLIssuer(appName string) string { + if !isSSLEnabled(appName) { + return "" + } + + for _, line := range strings.Split(opensslCertText(appName), "\n") { + if strings.Contains(line, "Issuer:") { + line = strings.Replace(line, "Issuer: ", "", 1) + return strings.TrimLeft(line, " \t") + } + } + + return "" +} + +func reportSSLSubject(appName string) string { + if !isSSLEnabled(appName) { + return "" + } + + result, err := common.CallExecCommand(common.ExecCommandInput{ + Command: "openssl", + Args: []string{"x509", "-in", filepath.Join(certTLSPath(appName), "server.crt"), "-noout", "-subject"}, + }) + if err != nil { + return "" + } + + subject := strings.Replace(result.StdoutContents(), "subject= ", "", 1) + subject = strings.TrimPrefix(subject, "/") + return strings.ReplaceAll(subject, "/", "; ") +} + +func reportSSLVerified(appName string) string { + if !isSSLEnabled(appName) { + return "" + } + + tlsPath := certTLSPath(appName) + args := []string{"verify", "-verbose", "-purpose", "sslserver"} + if _, err := os.Stat(filepath.Join(tlsPath, "server.letsencrypt.crt")); err == nil { + args = append(args, "-CAfile", filepath.Join(tlsPath, "server.crt"), filepath.Join(tlsPath, "server.letsencrypt.crt")) + } else { + args = append(args, filepath.Join(tlsPath, "server.crt")) + } + + result, _ := common.CallExecCommand(common.ExecCommandInput{ + Command: "openssl", + Args: args, + }) + + verifyOutput := "" + if out := strings.TrimRight(result.Stdout, "\n"); out != "" { + lines := strings.Split(out, "\n") + parts := strings.Split(lines[len(lines)-1], ":") + if len(parts) >= 2 { + verifyOutput = strings.TrimSpace(parts[1]) + } + } + + if verifyOutput == "OK" { + return "verified by a certificate authority" + } + + return "self signed" +} + +func reportSSLHostnames(appName string) string { + if !isSSLEnabled(appName) { + return "" + } + + hostnameSet := map[string]bool{} + + subjectResult, err := common.CallExecCommand(common.ExecCommandInput{ + Command: "openssl", + Args: []string{"x509", "-in", filepath.Join(certTLSPath(appName), "server.crt"), "-noout", "-subject"}, + }) + if err == nil { + for _, part := range strings.Split(subjectResult.StdoutContents(), "/") { + if strings.Contains(part, "CN=") && len(part) > 3 { + hostnameSet[part[3:]] = true + } + } + } + + textLines := strings.Split(opensslCertText(appName), "\n") + for i, line := range textLines { + if strings.Contains(line, "509v3 Subject Alternative Name:") && i+1 < len(textLines) { + sanLine := dnsPrefixRegex.ReplaceAllString(textLines[i+1], "") + for _, name := range strings.Split(sanLine, ",") { + name = strings.TrimSpace(name) + if name != "" { + hostnameSet[name] = true + } + } + } + } + + hostnames := []string{} + for name := range hostnameSet { + hostnames = append(hostnames, name) + } + sort.Strings(hostnames) + + return strings.Join(hostnames, " ") +} diff --git a/plugins/certs/src/subcommands/subcommands.go b/plugins/certs/src/subcommands/subcommands.go new file mode 100644 index 000000000..4027b90b8 --- /dev/null +++ b/plugins/certs/src/subcommands/subcommands.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "os" + "strings" + + certs "github.com/dokku/dokku/plugins/certs" + "github.com/dokku/dokku/plugins/common" + + flag "github.com/spf13/pflag" +) + +// main entrypoint to all subcommands +func main() { + parts := strings.Split(os.Args[0], "/") + subcommand := parts[len(parts)-1] + + var err error + switch subcommand { + case "report": + args := flag.NewFlagSet("certs:report", flag.ExitOnError) + format := args.String("format", "stdout", "format: [ stdout | json ]") + reportArgs, flagErr := common.ParseReportArgs("certs", os.Args[2:]) + if flagErr == nil { + args.Parse(reportArgs.OSArgs) + appName := args.Arg(0) + if reportArgs.IsGlobal { + appName = "--global" + } + err = certs.CommandReport(appName, *format, reportArgs.InfoFlag) + } + default: + err = fmt.Errorf("Invalid plugin subcommand call: %s", subcommand) + } + + if err != nil { + common.LogFailWithError(err) + } +} diff --git a/plugins/certs/src/triggers/triggers.go b/plugins/certs/src/triggers/triggers.go new file mode 100644 index 000000000..245fa8038 --- /dev/null +++ b/plugins/certs/src/triggers/triggers.go @@ -0,0 +1,31 @@ +package main + +import ( + "flag" + "fmt" + "os" + "strings" + + certs "github.com/dokku/dokku/plugins/certs" + "github.com/dokku/dokku/plugins/common" +) + +// main entrypoint to all triggers +func main() { + parts := strings.Split(os.Args[0], "/") + trigger := parts[len(parts)-1] + flag.Parse() + + var err error + switch trigger { + case "report": + appName := flag.Arg(0) + err = certs.ReportSingleApp(appName, "", "") + default: + err = fmt.Errorf("Invalid plugin trigger call: %s", trigger) + } + + if err != nil { + common.LogFailWithError(err) + } +} diff --git a/plugins/certs/subcommands.go b/plugins/certs/subcommands.go new file mode 100644 index 000000000..56521baf3 --- /dev/null +++ b/plugins/certs/subcommands.go @@ -0,0 +1,29 @@ +package certs + +import ( + "errors" + + "github.com/dokku/dokku/plugins/common" +) + +// CommandReport displays a certs report for one or more apps +func CommandReport(appName string, format string, infoFlag string) error { + if len(appName) == 0 { + apps, err := common.DokkuApps() + if err != nil { + if errors.Is(err, common.NoAppsExist) { + common.LogWarn(err.Error()) + return nil + } + return err + } + for _, appName := range apps { + if err := ReportSingleApp(appName, format, infoFlag); err != nil { + return err + } + } + return nil + } + + return ReportSingleApp(appName, format, infoFlag) +} diff --git a/plugins/certs/subcommands/report b/plugins/certs/subcommands/report deleted file mode 100755 index 054bbf7ca..000000000 --- a/plugins/certs/subcommands/report +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -source "$PLUGIN_AVAILABLE_PATH/certs/internal-functions" -set -eo pipefail -[[ $DOKKU_TRACE ]] && set -x - -cmd-certs-report "$@"