Skip to content
Snippets Groups Projects
Select Git revision
  • 8fcde66b5579791cf6531dd1df6e9a7f3ab0b985
  • main default protected
  • v0.8.6
  • v0.8.5
  • v0.8.1
  • v0.8.0
  • v0.7.2
  • v0.7.1
  • v0.7.0
  • v0.6.1
  • v0.6
  • v0.5.2
  • v0.5.1
  • v0.5
14 results

main.go

Blame
  • 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))
    	}
    }