242 lines
8.0 KiB
Go
242 lines
8.0 KiB
Go
package main
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hexops/gotextdiff"
|
|
"github.com/hexops/gotextdiff/myers"
|
|
"github.com/hexops/gotextdiff/span"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type Config struct {
|
|
Listen []string `yaml:"listen"`
|
|
Checkpoints string `yaml:"checkpoints"`
|
|
Roots string `yaml:"roots"`
|
|
Logs []Log `yaml:"logs"`
|
|
}
|
|
|
|
type Log struct {
|
|
ShortName string `yaml:"shortname"`
|
|
Inception string `yaml:"inception"`
|
|
Period int `yaml:"period"`
|
|
PoolSize int `yaml:"poolsize"`
|
|
SubmissionPrefix string `yaml:"submissionprefix"`
|
|
MonitoringPrefix string `yaml:"monitoringprefix"`
|
|
CCadbRoots string `yaml:"ccadbroots"`
|
|
ExtraRoots string `yaml:"extraroots"`
|
|
Secret string `yaml:"secret"`
|
|
Cache string `yaml:"cache"`
|
|
LocalDirectory string `yaml:"localdirectory"`
|
|
Listen string `yaml:"listen"`
|
|
NotAfterStart time.Time `yaml:"notafterstart"`
|
|
NotAfterLimit time.Time `yaml:"notafterlimit"`
|
|
// Computed fields
|
|
LogID string
|
|
PublicKeyPEM string
|
|
PublicKeyDERB64 string
|
|
PublicKeyBase64 string
|
|
Origin string
|
|
}
|
|
|
|
func main() {
|
|
configFile := flag.String("c", "./tesseract-staging.yaml", "Path to the YAML configuration file")
|
|
wantDiff := flag.Bool("diff", false, "Show unified diff of changes")
|
|
allowWrite := flag.Bool("write", false, "Allow writing files (required for actual file modifications)")
|
|
noColor := flag.Bool("no-color", false, "Disable colored diff output")
|
|
flag.Parse()
|
|
|
|
args := flag.Args()
|
|
if len(args) == 0 {
|
|
showHelp()
|
|
return
|
|
}
|
|
|
|
switch args[0] {
|
|
case "gen-html":
|
|
generateHTML(*configFile, *wantDiff, *allowWrite, !*noColor)
|
|
case "gen-env":
|
|
generateEnv(*configFile, *wantDiff, *allowWrite, !*noColor)
|
|
case "gen-key":
|
|
generateKeys(*configFile, *wantDiff, *allowWrite, !*noColor)
|
|
case "gen-nginx":
|
|
generateNginx(*configFile, *wantDiff, *allowWrite, !*noColor)
|
|
case "gen-roots":
|
|
generateRoots(args[1:], *wantDiff, *allowWrite, !*noColor)
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "Unknown command: %s\n", args[0])
|
|
showHelp()
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func loadConfig(yamlFile string) Config {
|
|
data, err := os.ReadFile(yamlFile)
|
|
if err != nil {
|
|
log.Fatalf("Failed to read YAML file: %v", err)
|
|
}
|
|
|
|
var config Config
|
|
err = yaml.Unmarshal(data, &config)
|
|
if err != nil {
|
|
log.Fatalf("Failed to parse YAML: %v", err)
|
|
}
|
|
|
|
// Set default listen port if not configured
|
|
if len(config.Listen) == 0 {
|
|
config.Listen = []string{":8080"}
|
|
}
|
|
|
|
// Set defaults for log entries
|
|
for i := range config.Logs {
|
|
if config.Logs[i].PoolSize == 0 {
|
|
config.Logs[i].PoolSize = 750
|
|
}
|
|
if config.Logs[i].Period == 0 {
|
|
config.Logs[i].Period = 200
|
|
}
|
|
|
|
// Extract hostname from SubmissionPrefix to set Origin
|
|
if config.Logs[i].SubmissionPrefix != "" {
|
|
hostname, err := extractHostname(config.Logs[i].SubmissionPrefix)
|
|
if err != nil {
|
|
log.Fatalf("Failed to parse SubmissionPrefix URL for %s: %v", config.Logs[i].ShortName, err)
|
|
}
|
|
config.Logs[i].Origin = hostname
|
|
}
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
// ANSI color codes
|
|
const (
|
|
colorReset = "\033[0m"
|
|
colorRed = "\033[31m"
|
|
colorGreen = "\033[32m"
|
|
colorYellow = "\033[33m"
|
|
colorCyan = "\033[36m"
|
|
)
|
|
|
|
// colorizeUnifiedDiff adds ANSI color codes to unified diff output
|
|
func colorizeUnifiedDiff(diff string) string {
|
|
lines := strings.Split(diff, "\n")
|
|
var colorizedLines []string
|
|
|
|
for _, line := range lines {
|
|
switch {
|
|
case strings.HasPrefix(line, "---"):
|
|
// File deletion header in cyan
|
|
colorizedLines = append(colorizedLines, colorCyan+line+colorReset)
|
|
case strings.HasPrefix(line, "+++"):
|
|
// File addition header in cyan
|
|
colorizedLines = append(colorizedLines, colorCyan+line+colorReset)
|
|
case strings.HasPrefix(line, "@@"):
|
|
// Hunk header in yellow
|
|
colorizedLines = append(colorizedLines, colorYellow+line+colorReset)
|
|
case strings.HasPrefix(line, "-"):
|
|
// Deleted lines in red
|
|
colorizedLines = append(colorizedLines, colorRed+line+colorReset)
|
|
case strings.HasPrefix(line, "+"):
|
|
// Added lines in green
|
|
colorizedLines = append(colorizedLines, colorGreen+line+colorReset)
|
|
default:
|
|
// Context lines unchanged
|
|
colorizedLines = append(colorizedLines, line)
|
|
}
|
|
}
|
|
|
|
return strings.Join(colorizedLines, "\n")
|
|
}
|
|
|
|
func writeFileWithStatus(filename string, content []byte, wantDiff bool, allowWrite bool, useColor bool) error {
|
|
existingContent, err := os.ReadFile(filename)
|
|
isNew := os.IsNotExist(err)
|
|
isUnchanged := false
|
|
|
|
if isNew {
|
|
if allowWrite {
|
|
fmt.Printf("Creating %s\n", filename)
|
|
} else {
|
|
fmt.Printf("Would create %s\n", filename)
|
|
}
|
|
} else if err != nil {
|
|
return fmt.Errorf("failed to read existing file %s: %v", filename, err)
|
|
} else if string(existingContent) == string(content) {
|
|
fmt.Printf("Unchanged %s\n", filename)
|
|
isUnchanged = true
|
|
} else {
|
|
if allowWrite {
|
|
fmt.Printf("Updating %s\n", filename)
|
|
} else {
|
|
fmt.Printf("Would update %s\n", filename)
|
|
}
|
|
}
|
|
|
|
if wantDiff && !isUnchanged {
|
|
if isNew {
|
|
// For new files, show the entire content as added
|
|
edits := myers.ComputeEdits(span.URIFromPath(filename), "", string(content))
|
|
diff := fmt.Sprint(gotextdiff.ToUnified("/dev/null", filename, "", edits))
|
|
if useColor {
|
|
fmt.Print(colorizeUnifiedDiff(diff))
|
|
} else {
|
|
fmt.Print(diff)
|
|
}
|
|
} else {
|
|
// For existing files, show the diff
|
|
edits := myers.ComputeEdits(span.URIFromPath(filename), string(existingContent), string(content))
|
|
diff := fmt.Sprint(gotextdiff.ToUnified(filename, filename+".new", string(existingContent), edits))
|
|
if useColor {
|
|
fmt.Print(colorizeUnifiedDiff(diff))
|
|
} else {
|
|
fmt.Print(diff)
|
|
}
|
|
}
|
|
}
|
|
|
|
if isUnchanged || !allowWrite {
|
|
return nil
|
|
}
|
|
|
|
err = os.WriteFile(filename, content, 0644)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to write file %s: %v", filename, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func showHelp() {
|
|
fmt.Printf("Usage: %s [options] <command>\n\n", os.Args[0])
|
|
fmt.Printf("Note: Flags must come before the command name.\n\n")
|
|
fmt.Printf("Options:\n")
|
|
fmt.Printf(" -c <file> Path to YAML configuration file (default: ./tesseract-staging.yaml)\n")
|
|
fmt.Printf(" --diff Show unified diff of changes without writing files\n")
|
|
fmt.Printf(" --write Allow writing files (required for actual file modifications)\n")
|
|
fmt.Printf(" --no-color Disable colored diff output\n\n")
|
|
fmt.Printf("Examples:\n")
|
|
fmt.Printf(" %s --diff gen-html # Show colored diffs without writing\n", os.Args[0])
|
|
fmt.Printf(" %s --diff --no-color gen-html # Show plain diffs without writing\n", os.Args[0])
|
|
fmt.Printf(" %s --write gen-html # Write files\n", os.Args[0])
|
|
fmt.Printf(" %s --diff --write gen-html # Show colored diffs and write files\n\n", os.Args[0])
|
|
fmt.Printf("Commands:\n")
|
|
fmt.Printf(" gen-html Generate index.html and log.v3.json files in each log's localdirectory.\n")
|
|
fmt.Printf(" Creates HTML pages with log information and CT log metadata JSON.\n")
|
|
fmt.Printf(" Computes LOG_ID and public keys from private keys.\n\n")
|
|
fmt.Printf(" gen-env Generate .env files and combined roots.pem in each log's localdirectory.\n")
|
|
fmt.Printf(" Creates TESSERACT_ARGS environment variable with command line flags.\n")
|
|
fmt.Printf(" Combines global roots and log-specific extraroots into roots.pem.\n\n")
|
|
fmt.Printf(" gen-key Generate prime256v1 private keys for each log (only if they don't exist).\n")
|
|
fmt.Printf(" Creates EC private key files at the path specified in log.secret.\n\n")
|
|
fmt.Printf(" gen-nginx Generate nginx configuration files for each log's monitoring endpoint.\n")
|
|
fmt.Printf(" Creates nginx-<hostname>.conf files in each log's localdirectory.\n\n")
|
|
fmt.Printf(" gen-roots Download root certificates from a Certificate Transparency log.\n")
|
|
fmt.Printf(" Options: --source <url> (default: https://rennet2027h2.log.ct.ipng.ch/)\n")
|
|
fmt.Printf(" --output <file> (default: roots.pem)\n\n")
|
|
}
|