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
Branches
Tags
No related merge requests found
......@@ -28,7 +28,7 @@ LATEST_TAG=$(git describe --tags $(git rev-list --tags --max-count=1))
PROG_VERSION=${LATEST_TAG:1}
BUILD_TIME=$(date -u '+%Y-%m-%d_%H:%M:%S')
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
......
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
import (
"acme-web/certinspector"
"fmt"
"log"
"net/http"
......@@ -16,13 +17,16 @@ import (
var (
appVersion string
buildTime string
baseDir string
webDir string
jsonConverter string
bearerToken string
WarningLogger *log.Logger
InfoLogger *log.Logger
ErrorLogger *log.Logger
verboseBool bool
baseURLs []string
apiURLs []string
otherURLs []string
)
func init() {
......@@ -33,37 +37,39 @@ func init() {
// serve certificates JSON
func renderJSON(w http.ResponseWriter, req *http.Request) {
// content-type currently not working
provider := strings.Split(req.URL.Path, "/")[2]
serveFile := fmt.Sprintf("%v/%v/%v.json", webDir, provider, provider)
cmd := exec.Command(jsonConverter, "-p", provider)
err := cmd.Run()
jsonData, err := certinspector.ProcessCertificates(baseDir, provider)
if err != nil {
WarningLogger.Println(err)
w.WriteHeader(http.StatusServiceUnavailable)
} else {
if verboseBool {
InfoLogger.Printf("HTTP Status %v", http.StatusOK)
}
w.Header().Set("Content-Type", "application/json")
http.Error(w, "Failed to process certificates", http.StatusServiceUnavailable)
return
}
// Write JSON response
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
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)
cmd := exec.Command(jsonConverter, "-p", provider[1])
err := cmd.Run()
err := certinspector.ProcessCertificatesWrite(baseDir, provider, outputDir)
if err != nil {
WarningLogger.Println(err)
w.WriteHeader(http.StatusServiceUnavailable)
} else {
if verboseBool {
InfoLogger.Printf("HTTP Status %v", http.StatusOK)
}
w.Header().Set("Content-Type", "text/html")
http.Error(w, "Failed to process certificates", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "text/html")
if verboseBool {
InfoLogger.Printf("Serving file: %s", serveFile)
}
http.ServeFile(w, req, serveFile)
}
......@@ -112,7 +118,7 @@ func main() {
- serve ACME HTML pages, trigger Puppet, expose API
Usage:
%v [--json-converter=JSONCONVERTER] [--listen-address=LISTENADDRESS] [--listen-port=LISTENPORT] [--verbose]
%v [--listen-address=LISTENADDRESS] [--listen-port=LISTENPORT] [--verbose]
%v -h | --help
%v -b | --build
%v -v | --version
......@@ -121,7 +127,6 @@ Options:
-h --help Show this screen
-b --build Print version and build 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-port=LISTENPORT Web server port [default: 8000]
--verbose Log also successful connections
......@@ -140,22 +145,37 @@ Options:
os.Exit(1)
}
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"
jsonConverter = arguments["--json-converter"].(string)
verboseBool = arguments["--verbose"].(bool)
listenAddress := arguments["--listen-address"].(string)
listenPort := arguments["--listen-port"].(string)
baseURLs := [6]string{"/sectigo_ev", "/sectigo_ov", "/letsencrypt", "/sectigo_ev/", "/sectigo_ov/", "/letsencrypt/"}
apiURLs := [6]string{"/api/sectigo_ev", "/api/sectigo_ov", "/api/letsencrypt",
"/api/sectigo_ev/", "/api/sectigo_ov/", "/api/letsencrypt/"}
otherURLs := [12]string{"/letsencrypt/by_name.html", "/letsencrypt/by_date.html",
"/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",
"/sectigo_ev/by_name.html", "/sectigo_ev/by_date.html",
"/sectigo_ev/sectigo_ev.json", "/sectigo_ev/sectigo_ev_expired.json"}
for _, provider := range acmeProviders {
fmt.Printf("%v element\n", provider)
baseURLs = append(baseURLs, "/"+provider, "/"+provider+"/")
fmt.Printf("%v element\n", baseURLs)
}
for _, provider := range acmeProviders {
apiURLs = append(apiURLs, "/api/"+provider, "/api/"+provider+"/")
}
for _, provider := range acmeProviders {
otherURLs = append(
otherURLs, "/"+provider+"/by_name.html", "/"+provider+"/by_date.html",
"/"+provider+"/"+provider+".json", "/"+provider+"/"+provider+"_expired.json",
)
}
puppetURL := "/puppet"
fs := http.FileServer(http.Dir("/var/www/acme_web/static"))
......@@ -184,5 +204,4 @@ Options:
} else {
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