Select Git revision
Max Adamo authored
main.go 7.69 KiB
package main
import (
"acme-web/certinspector"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"github.com/docopt/docopt-go"
"gopkg.in/ini.v1"
)
var (
appVersion string
buildTime string
baseDir string
webDir string
bearerToken string
DebugLogger *log.Logger
InfoLogger *log.Logger
WarningLogger *log.Logger
ErrorLogger *log.Logger
verboseBool bool
baseURLs []string
apiURLs []string
otherURLs []string
)
func init() {
DebugLogger = log.New(os.Stdout, "DEBUG: ", log.Ldate|log.Ltime)
InfoLogger = log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime)
WarningLogger = log.New(os.Stdout, "WARNING: ", log.Ldate|log.Ltime)
ErrorLogger = log.New(os.Stdout, "ERROR: ", log.Ldate|log.Ltime)
}
// serve certificates JSON
func renderJSON(w http.ResponseWriter, req *http.Request) {
provider := strings.Split(req.URL.Path, "/")[2]
jsonData, err := certinspector.ProcessCertificates(baseDir, provider, verboseBool)
if err != nil {
WarningLogger.Println(err)
http.Error(w, "Failed to process certificates", http.StatusServiceUnavailable)
return
}
if verboseBool {
DebugLogger.Printf("JSON generation initiated for provider: %s", provider)
}
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)
}
}
// serve certificates list
func renderPage(w http.ResponseWriter, req *http.Request) {
provider := strings.Split(req.URL.Path, "/")[1]
outputDir := filepath.Join(webDir, provider)
serveFile := filepath.Join(webDir, req.URL.Path)
suffix := ""
if strings.HasSuffix(serveFile, "/by_date.html") {
suffix = "_expired"
}
err := certinspector.ProcessCertificatesWrite(baseDir, provider, outputDir, verboseBool, suffix)
if err != nil {
WarningLogger.Println(err)
http.Error(w, "Failed to process certificates", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "text/html")
if verboseBool {
DebugLogger.Printf("Serving file: %s", serveFile)
}
http.ServeFile(w, req, serveFile)
}
// trigger puppet
// triggerPuppet triggers the Puppet process by sending SIGUSR1.
func triggerPuppet(w http.ResponseWriter, req *http.Request) {
const authHeaderPrefix = "Bearer "
cmd := exec.Command("/usr/bin/pkill", "-f", "/opt/puppetlabs/puppet/bin/puppet", "--signal", "SIGUSR1")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
authHeader := req.Header.Get("Authorization")
if !strings.HasPrefix(authHeader, authHeaderPrefix) {
http.Error(w, `{"status": "Unauthorized", "response": 401, "puppet": "NOT triggered"}`, http.StatusUnauthorized)
return
}
authToken := strings.TrimPrefix(authHeader, authHeaderPrefix)
if authToken != bearerToken {
http.Error(w, `{"status": "Unauthorized", "response": 401}`, http.StatusUnauthorized)
return
}
err := cmd.Run()
if err != nil {
WarningLogger.Printf("Failed to trigger Puppet: %v", err)
http.Error(w, `{"status": "KO", "response": 503, "error": "`+err.Error()+`"}`, http.StatusServiceUnavailable)
return
}
if verboseBool {
DebugLogger.Printf("HTTP Status %v - Puppet triggered successfully", http.StatusOK)
}
response := `{"status": "OK", "response": 200, "puppet": "triggered"}`
w.WriteHeader(http.StatusOK)
_, err = w.Write([]byte(response))
if err != nil {
WarningLogger.Printf("Failed to write response: %v", err)
}
}
// redirect to /by_name.html
func redirect(w http.ResponseWriter, req *http.Request) {
redirectURL := filepath.Join(req.URL.Path, "/by_name.html")
if verboseBool {
DebugLogger.Printf("redirecting to: %v", redirectURL)
}
http.Redirect(w, req, redirectURL, http.StatusMovedPermanently)
}
// Custom HTTP handler with 404 fallback
func customHandler(w http.ResponseWriter, req *http.Request) {
path := req.URL.Path
rootPath := []string{"/", "/index.html", "/index.htm"}
if slices.Contains(rootPath, path) {
// if verboseBool {
// DebugLogger.Printf("Serving file: %s", path) // }
http.ServeFile(w, req, filepath.Join(webDir, "index.html"))
return
}
if path == "/favicon.ico" {
http.ServeFile(w, req, filepath.Join(webDir, "static", "anvil.ico"))
}
if path == "/ping" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, err := w.Write([]byte(`{"status": "ok"}`))
if err != nil {
WarningLogger.Printf("Failed to write ping response: %v", err)
}
return
}
// Check if the path matches any valid routes
for _, apiElement := range apiURLs {
if path == apiElement {
renderJSON(w, req)
return
}
}
for _, element := range baseURLs {
if path == element {
redirect(w, req)
return
}
}
for _, otherElement := range otherURLs {
if path == otherElement {
renderPage(w, req)
return
}
}
if path == "/puppet" {
triggerPuppet(w, req)
return
}
if strings.HasPrefix(path, "/static/") {
fs := http.FileServer(http.Dir(filepath.Join(webDir, "static")))
http.StripPrefix("/static/", fs).ServeHTTP(w, req)
return
}
http.NotFound(w, req) // If no route matches, return 404
}
func main() {
progName := filepath.Base(os.Args[0])
usage := fmt.Sprintf(`ACME Web:
- serve ACME HTML pages, trigger Puppet, expose API
Usage:
%v [--listen-address=LISTENADDRESS] [--listen-port=LISTENPORT] [--verbose]
%v -h | --help
%v -b | --build
%v -v | --version
Options:
-h --help Show this screen
-b --build Print version and build information and exit
-v --version Print version information and exit
--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
`, progName, progName, progName, progName)
arguments, _ := docopt.ParseArgs(usage, nil, appVersion)
if arguments["--build"] == true {
fmt.Printf("%v version: %v, built on: %v\n", progName, appVersion, buildTime)
os.Exit(0)
}
cfg, err := ini.Load("/root/.acme.ini")
if err != nil {
fmt.Printf("Fail to read file: %v\n", err)
os.Exit(1)
}
bearerToken = cfg.Section("acme").Key("bearer_token").String()
acmeProvidersRaw := cfg.Section("acme").Key("acme_providers").String()
acmeProvidersRaw = strings.Trim(acmeProvidersRaw, "[] ") // remove leading and trailing brackets and spaces
acmeProviders := strings.Split(acmeProvidersRaw, ",") // split by comma
for i := range acmeProviders {
acmeProviders[i] = strings.Trim(acmeProviders[i], "' ")
}
baseDir = "/etc"
webDir = "/var/www/acme_web"
verboseBool = arguments["--verbose"].(bool)
listenAddress := arguments["--listen-address"].(string)
listenPort := arguments["--listen-port"].(string)
if verboseBool {
DebugLogger.Printf("Starting ACME Web server in debug mode")
} else {
InfoLogger.Printf("Starting ACME Web server")
}
for _, provider := range acmeProviders {
baseURLs = append(baseURLs, "/"+provider, "/"+provider+"/")
}
for _, provider := range acmeProviders {
otherURLs = append(
otherURLs, "/"+provider+"/by_name.html", "/"+provider+"/by_date.html",
"/"+provider+"/"+provider+".json", "/"+provider+"/"+provider+"_expired.json",
)
}
for _, provider := range acmeProviders {
apiURLs = append(apiURLs, "/api/"+provider, "/api/"+provider+"/")
}
if verboseBool {
DebugLogger.Printf("Serving baseURLs: %v", baseURLs)
DebugLogger.Printf("Serving apiURLs: %v", apiURLs)
DebugLogger.Printf("Serving otherURLs: %v", otherURLs)
}
http.HandleFunc("/", customHandler)
if listenAddress == "any" {
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", listenPort), nil))
} else {
log.Fatal(http.ListenAndServe(fmt.Sprintf("%v:%v", listenAddress, listenPort), nil))
}
}