Skip to content
Snippets Groups Projects
Unverified Commit 39aaacf1 authored by Max Adamo's avatar Max Adamo
Browse files

add certificate inspector functionality and update build command

parent 694102b9
No related branches found
No related tags found
No related merge requests found
...@@ -28,7 +28,7 @@ LATEST_TAG=$(git describe --tags $(git rev-list --tags --max-count=1)) ...@@ -28,7 +28,7 @@ LATEST_TAG=$(git describe --tags $(git rev-list --tags --max-count=1))
PROG_VERSION=${LATEST_TAG:1} PROG_VERSION=${LATEST_TAG:1}
BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S') BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S')
git checkout $LATEST_TAG git checkout $LATEST_TAG
go build -ldflags "-s -w -X main.appVersion=${PROG_VERSION} -X main.buildTime=${BUILDTIME}" -o acme-web GOARCH=amd64 GOOS=linux go build -ldflags "-s -w -X main.appVersion=${PROG_VERSION} -X main.buildTime=${BUILDTIME}" -o acme-web
``` ```
## Setting up systemd ## Setting up systemd
......
package certinspector
import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
)
type CertificateData struct {
CertName string `json:"certname"`
SerialNumber string `json:"serial_number"`
Domains []string `json:"domains"`
ExpiryDate string `json:"expiry_date"`
}
// inspect certificate and return CertificateData
func InspectCertificate(certDir string) (CertificateData, error) {
fullchainPath := filepath.Join(certDir, "fullchain.pem")
data, err := os.ReadFile(fullchainPath)
if err != nil {
return CertificateData{}, fmt.Errorf("failed to read certificate: %w", err)
}
block, _ := pem.Decode(data)
if block == nil {
return CertificateData{}, errors.New("failed to parse PEM block")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return CertificateData{}, fmt.Errorf("failed to parse certificate: %w", err)
}
certName := cert.Subject.CommonName
serial := strings.ToUpper(fmt.Sprintf("%X", cert.SerialNumber))
domains := cert.DNSNames
daysLeft := int(time.Until(cert.NotAfter).Hours() / 24)
status := "VALID"
if daysLeft <= 0 {
status = "EXPIRED"
}
expiry := fmt.Sprintf("%s: %d DAYS", status, daysLeft)
return CertificateData{
CertName: certName,
SerialNumber: serial,
Domains: domains,
ExpiryDate: expiry,
}, nil
}
// call writeJSON functio. Used by the API.
func ProcessCertificatesWrite(baseDir, provider string, outputDir string) error {
liveDir := filepath.Join(baseDir, provider, "live")
dirs, err := os.ReadDir(liveDir)
if err != nil {
return fmt.Errorf("failed to list directories: %w", err)
}
var wg sync.WaitGroup
var mu sync.Mutex
results := []CertificateData{}
for _, dir := range dirs {
if !dir.IsDir() || dir.Name() == "README" {
continue
}
wg.Add(1)
go func(certDir string) {
defer wg.Done()
data, err := InspectCertificate(certDir)
if err == nil {
mu.Lock()
results = append(results, data)
mu.Unlock()
}
}(filepath.Join(liveDir, dir.Name()))
}
wg.Wait()
sort.Slice(results, func(i, j int) bool {
return results[i].CertName < results[j].CertName
})
outputFile := filepath.Join(outputDir, provider+".json")
return writeJSON(outputFile, results)
}
// write JSON to file. Used by the API.
func writeJSON(filename string, data interface{}) error {
file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("failed to create JSON file: %w", err)
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
return encoder.Encode(data)
}
// process certificates and return JSON data
func ProcessCertificates(baseDir, provider string) ([]byte, error) {
liveDir := filepath.Join(baseDir, provider, "live")
dirs, err := os.ReadDir(liveDir)
if err != nil {
return nil, fmt.Errorf("failed to list directories: %w", err)
}
var wg sync.WaitGroup
var mu sync.Mutex
results := []CertificateData{}
for _, dir := range dirs {
if !dir.IsDir() || dir.Name() == "README" {
continue
}
wg.Add(1)
go func(certDir string) {
defer wg.Done()
data, err := InspectCertificate(certDir)
if err == nil {
mu.Lock()
results = append(results, data)
mu.Unlock()
}
}(filepath.Join(liveDir, dir.Name()))
}
wg.Wait()
sort.Slice(results, func(i, j int) bool {
return results[i].CertName < results[j].CertName
})
// Encode results directly into JSON
jsonData, err := json.MarshalIndent(results, "", " ")
if err != nil {
return nil, fmt.Errorf("failed to encode JSON: %w", err)
}
return jsonData, nil
}
package main package main
import ( import (
"acme-web/certinspector"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
...@@ -16,13 +17,16 @@ import ( ...@@ -16,13 +17,16 @@ import (
var ( var (
appVersion string appVersion string
buildTime string buildTime string
baseDir string
webDir string webDir string
jsonConverter string
bearerToken string bearerToken string
WarningLogger *log.Logger WarningLogger *log.Logger
InfoLogger *log.Logger InfoLogger *log.Logger
ErrorLogger *log.Logger ErrorLogger *log.Logger
verboseBool bool verboseBool bool
baseURLs []string
apiURLs []string
otherURLs []string
) )
func init() { func init() {
...@@ -33,37 +37,39 @@ func init() { ...@@ -33,37 +37,39 @@ func init() {
// serve certificates JSON // serve certificates JSON
func renderJSON(w http.ResponseWriter, req *http.Request) { func renderJSON(w http.ResponseWriter, req *http.Request) {
// content-type currently not working
provider := strings.Split(req.URL.Path, "/")[2] provider := strings.Split(req.URL.Path, "/")[2]
serveFile := fmt.Sprintf("%v/%v/%v.json", webDir, provider, provider) jsonData, err := certinspector.ProcessCertificates(baseDir, provider)
cmd := exec.Command(jsonConverter, "-p", provider)
err := cmd.Run()
if err != nil { if err != nil {
WarningLogger.Println(err) WarningLogger.Println(err)
w.WriteHeader(http.StatusServiceUnavailable) http.Error(w, "Failed to process certificates", http.StatusServiceUnavailable)
} else { return
if verboseBool { }
InfoLogger.Printf("HTTP Status %v", http.StatusOK)
} // Write JSON response
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, err = w.Write(jsonData)
if err != nil {
WarningLogger.Printf("Failed to write response: %v", err)
} }
http.ServeFile(w, req, serveFile)
} }
// serve certificates list // serve certificates list
func renderPage(w http.ResponseWriter, req *http.Request) { func renderPage(w http.ResponseWriter, req *http.Request) {
provider := strings.Split(req.URL.Path, "/") provider := strings.Split(req.URL.Path, "/")[1]
outputDir := filepath.Join(webDir, provider)
serveFile := filepath.Join(webDir, req.URL.Path) serveFile := filepath.Join(webDir, req.URL.Path)
cmd := exec.Command(jsonConverter, "-p", provider[1])
err := cmd.Run() err := certinspector.ProcessCertificatesWrite(baseDir, provider, outputDir)
if err != nil { if err != nil {
WarningLogger.Println(err) WarningLogger.Println(err)
w.WriteHeader(http.StatusServiceUnavailable) http.Error(w, "Failed to process certificates", http.StatusServiceUnavailable)
} else { return
if verboseBool { }
InfoLogger.Printf("HTTP Status %v", http.StatusOK)
} w.Header().Set("Content-Type", "text/html")
w.Header().Set("Content-Type", "text/html") if verboseBool {
InfoLogger.Printf("Serving file: %s", serveFile)
} }
http.ServeFile(w, req, serveFile) http.ServeFile(w, req, serveFile)
} }
...@@ -112,7 +118,7 @@ func main() { ...@@ -112,7 +118,7 @@ func main() {
- serve ACME HTML pages, trigger Puppet, expose API - serve ACME HTML pages, trigger Puppet, expose API
Usage: Usage:
%v [--json-converter=JSONCONVERTER] [--listen-address=LISTENADDRESS] [--listen-port=LISTENPORT] [--verbose] %v [--listen-address=LISTENADDRESS] [--listen-port=LISTENPORT] [--verbose]
%v -h | --help %v -h | --help
%v -b | --build %v -b | --build
%v -v | --version %v -v | --version
...@@ -121,7 +127,6 @@ Options: ...@@ -121,7 +127,6 @@ Options:
-h --help Show this screen -h --help Show this screen
-b --build Print version and build information and exit -b --build Print version and build information and exit
-v --version Print version information and exit -v --version Print version information and exit
--json-converter=JSONCONVERTER Path to json converter script [default: /usr/bin/cert2json]
--listen-address=LISTENADDRESS Web server address. Check Go net/http documentation [default: any] --listen-address=LISTENADDRESS Web server address. Check Go net/http documentation [default: any]
--listen-port=LISTENPORT Web server port [default: 8000] --listen-port=LISTENPORT Web server port [default: 8000]
--verbose Log also successful connections --verbose Log also successful connections
...@@ -140,22 +145,37 @@ Options: ...@@ -140,22 +145,37 @@ Options:
os.Exit(1) os.Exit(1)
} }
bearerToken = cfg.Section("acme").Key("bearer_token").String() bearerToken = cfg.Section("acme").Key("bearer_token").String()
// remove leading and trailing spaces and quotes and split by comma
acmeProvidersRaw := cfg.Section("acme").Key("acme_providers").String()
acmeProvidersRaw = strings.Trim(acmeProvidersRaw, "[] ")
acmeProviders := strings.Split(acmeProvidersRaw, ",")
for i := range acmeProviders {
acmeProviders[i] = strings.Trim(acmeProviders[i], "' ")
}
baseDir = "/etc"
webDir = "/var/www/acme_web" webDir = "/var/www/acme_web"
jsonConverter = arguments["--json-converter"].(string)
verboseBool = arguments["--verbose"].(bool) verboseBool = arguments["--verbose"].(bool)
listenAddress := arguments["--listen-address"].(string) listenAddress := arguments["--listen-address"].(string)
listenPort := arguments["--listen-port"].(string) listenPort := arguments["--listen-port"].(string)
baseURLs := [6]string{"/sectigo_ev", "/sectigo_ov", "/letsencrypt", "/sectigo_ev/", "/sectigo_ov/", "/letsencrypt/"} for _, provider := range acmeProviders {
apiURLs := [6]string{"/api/sectigo_ev", "/api/sectigo_ov", "/api/letsencrypt", fmt.Printf("%v element\n", provider)
"/api/sectigo_ev/", "/api/sectigo_ov/", "/api/letsencrypt/"} baseURLs = append(baseURLs, "/"+provider, "/"+provider+"/")
otherURLs := [12]string{"/letsencrypt/by_name.html", "/letsencrypt/by_date.html", fmt.Printf("%v element\n", baseURLs)
"/letsencrypt/letsencrypt.json", "/letsencrypt/letsencrypt_expired.json", }
"/sectigo_ov/by_name.html", "/sectigo_ov/by_date.html",
"/sectigo_ov/sectigo_ov.json", "/sectigo_ov/sectigo_ov_expired.json", for _, provider := range acmeProviders {
"/sectigo_ev/by_name.html", "/sectigo_ev/by_date.html", apiURLs = append(apiURLs, "/api/"+provider, "/api/"+provider+"/")
"/sectigo_ev/sectigo_ev.json", "/sectigo_ev/sectigo_ev_expired.json"} }
for _, provider := range acmeProviders {
otherURLs = append(
otherURLs, "/"+provider+"/by_name.html", "/"+provider+"/by_date.html",
"/"+provider+"/"+provider+".json", "/"+provider+"/"+provider+"_expired.json",
)
}
puppetURL := "/puppet" puppetURL := "/puppet"
fs := http.FileServer(http.Dir("/var/www/acme_web/static")) fs := http.FileServer(http.Dir("/var/www/acme_web/static"))
...@@ -184,5 +204,4 @@ Options: ...@@ -184,5 +204,4 @@ Options:
} else { } else {
log.Fatal(http.ListenAndServe(fmt.Sprintf("%v:%v", listenAddress, listenPort), nil)) log.Fatal(http.ListenAndServe(fmt.Sprintf("%v:%v", listenAddress, listenPort), nil))
} }
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment