package main import ( "github.com/fsnotify/fsnotify" "flag" "log" "os" "io" "encoding/json" "time" "path" "path/filepath" "os/user" "os/exec" "strconv" ) type Cert struct { PrivateKey string PublicKey string Owner string } type Config struct { Source Cert Target Cert Reload []string WatchDelay string watcher *fsnotify.Watcher delay_time time.Duration } func MustExist(p string) { _, err := os.Stat(p) if os.IsNotExist(err) { log.Fatalf("path %s does not exist: %v", p, err) } } func AbsPath(p string) string { p, err := filepath.Abs(p) if err != nil { log.Fatalf("can't convert %s to absolute p: %v", p, err) } return p } func LoadConfig(config_path string) Config { var config Config config_data, err := os.ReadFile(config_path) if err != nil { log.Fatal("invalid config path %s: %v", config_path, err) } err = json.Unmarshal(config_data, &config) if err != nil { log.Fatal(err, "json format error") } config.delay_time, err = time.ParseDuration(config.WatchDelay) if err != nil { log.Fatalf("can't parse watch_delay setting %s: %v", config.WatchDelay, err) } config.Source.PrivateKey = AbsPath(config.Source.PrivateKey) MustExist(config.Source.PrivateKey) config.Source.PublicKey = AbsPath(config.Source.PublicKey) MustExist(config.Source.PublicKey) config.Target.PrivateKey = AbsPath(config.Target.PrivateKey) MustExist(path.Dir(config.Target.PrivateKey)) config.Target.PublicKey = AbsPath(config.Target.PublicKey) MustExist(path.Dir(config.Target.PublicKey)) return config } func ParseOpts() Config { var config_file string flag.StringVar(&config_file, "config", "cert-bouncer.json", ".json config to use.") flag.Parse() return LoadConfig(config_file) } func ChownTarget(fname string, owner string) { u, err := user.Lookup(owner) if err != nil { log.Fatalf("failed to find owner %s: %v", fname, err) } uid, err := strconv.Atoi(u.Uid) if err != nil { log.Fatalf("UID %s gives bad result when calling user.Lookup()", u.Uid) } gid, err := strconv.Atoi(u.Gid) if err != nil { log.Fatalf("GID %s gives bad result when calling user.Lookup()", u.Gid) } err = os.Chown(fname, uid, gid) if err != nil { log.Fatalf("Error cannot chown file %s to user %s: %v", fname, owner, err) } } func Copy(from string, to string, owner string) { log.Println("copying from=", from, "to=", to) src, err := os.Open(from) if err != nil { log.Fatalf("%s: %v", from, err) } defer src.Close() dst, err := os.OpenFile(to, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { log.Fatalf("%s: %v", to, err) } defer dst.Close() _, err = io.Copy(dst, src) ChownTarget(to, owner) if err != nil { log.Fatalf("failed to copy: %v", err) } } func (cfg *Config) RunReload() { exe := cfg.Reload[0] args := cfg.Reload[1:] cmd := exec.Command(exe, args...) stdout, err := cmd.StdoutPipe() go func() { _, err := io.Copy(os.Stdout, stdout) if err != nil { log.Fatalf("Can't log output") } }() err = cmd.Run() if err != nil { log.Fatalf("failed to run %v: %v", cfg.Reload, err) } } func (cfg *Config) SyncCerts() { log.Println("SYNC CERTS CALLED") // copy the files, also changes ownership of target Copy(cfg.Source.PrivateKey, cfg.Target.PrivateKey, cfg.Target.Owner) Copy(cfg.Source.PublicKey, cfg.Target.PublicKey, cfg.Target.Owner) cfg.RunReload() } func (cfg *Config) HandleEvents() { doit := time.NewTimer(cfg.delay_time) doit.Stop() for { select { case event, ok := <-cfg.watcher.Events: if !ok { return } log.Println("EVENT", event) if event.Name == cfg.Source.PrivateKey { doit.Reset(cfg.delay_time) } case <-doit.C: cfg.SyncCerts() case err, ok := <-cfg.watcher.Errors: if !ok { return } log.Println("failed to watch", err) } } } func (cfg *Config) WatchFiles() { var err error cfg.watcher, err = fsnotify.NewWatcher() if err != nil { log.Fatal(err, "Can't watch files.") } defer cfg.watcher.Close() go cfg.HandleEvents() err = cfg.watcher.Add(cfg.Source.PrivateKey) if err != nil { log.Fatalf("can't watch %s: %v", err, cfg.Source.PrivateKey) } <-make(chan struct{}) } func main() { config := ParseOpts() config.WatchFiles() }