package tlsservice import ( "errors" "io" "os" "strings" "time" ) // RenewalLogPath is written only by the Certbot deploy hook after Nginx reload. const RenewalLogPath = "/var/log/opsx-vpn/cert-renewals.log" type RenewalEvent struct { Time time.Time `json:"time"` Host string `json:"host"` } // ReadRenewalEvents returns a bounded, validated view of successful renewals. // Missing history does not imply a renewal failure: the certificate may not be due. func ReadRenewalEvents() []RenewalEvent { return readRenewalEvents(RenewalLogPath) } func readRenewalEvents(path string) []RenewalEvent { f, err := os.Open(path) if err != nil { return nil } defer f.Close() const limit int64 = 65536 info, err := f.Stat() if err != nil { return nil } start := info.Size() - limit if start < 0 { start = 0 } if _, err := f.Seek(start, io.SeekStart); err != nil { return nil } data, err := io.ReadAll(io.LimitReader(f, limit)) if err != nil && !errors.Is(err, io.EOF) { return nil } lines := strings.Split(string(data), "\n") if start > 0 && len(lines) > 0 { lines = lines[1:] // ignore a partial line from the bounded tail } var events []RenewalEvent for i := len(lines) - 1; i >= 0 && len(events) < 20; i-- { fields := strings.Split(lines[i], "\t") if len(fields) != 2 || (!ValidPublicHost("https", fields[1]) && !ValidDomain(fields[1])) { continue } stamp, err := time.Parse(time.RFC3339, fields[0]) if err == nil { events = append(events, RenewalEvent{Time: stamp, Host: fields[1]}) } } return events }