1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
package tlsreload
import (
"crypto/tls"
"errors"
"fmt"
"log/slog"
"os"
"slices"
"sync"
"time"
)
type stamp struct {
mod time.Time
size int64
ok bool
}
type Reloader struct {
certFile string
keyFiles []string
watch []string
log *slog.Logger
mu sync.Mutex
cert *tls.Certificate
stamps []stamp
}
func New(certFile string, keyFiles []string, log *slog.Logger) (*Reloader, error) {
if certFile == "" {
return nil, errors.New("tlsreload: no certificate file")
}
if log == nil {
log = slog.New(slog.DiscardHandler)
}
r := &Reloader{certFile: certFile, keyFiles: append([]string(nil), keyFiles...), log: log}
r.watch = r.watchPaths()
r.stamps = r.stampsNow()
cert, err := loadPair(certFile, r.keyFiles)
if err != nil {
return nil, err
}
r.cert = cert
return r, nil
}
func (r *Reloader) TLSConfig(base *tls.Config) *tls.Config {
if base == nil {
base = &tls.Config{}
}
cfg := base.Clone()
cfg.Certificates = nil
cfg.GetCertificate = r.GetCertificate
return cfg
}
func (r *Reloader) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {
r.mu.Lock()
defer r.mu.Unlock()
if stamps := r.stampsNow(); !sameStamps(stamps, r.stamps) {
cert, err := loadPair(r.certFile, r.keyFiles)
if err != nil {
r.log.Warn("keeping the last certificate", "cert", r.certFile, "err", err)
} else {
r.cert = cert
r.log.Info("reloaded the certificate", "cert", r.certFile)
}
r.stamps = stamps
}
if r.cert == nil {
return nil, fmt.Errorf("tlsreload: no certificate loaded from %s", r.certFile)
}
return r.cert, nil
}
func (r *Reloader) watchPaths() []string {
out := []string{r.certFile}
for _, path := range r.keyFiles {
if path == "" || path == r.certFile {
continue
}
dup := slices.Contains(out, path)
if !dup {
out = append(out, path)
}
}
return out
}
func (r *Reloader) stampsNow() []stamp {
out := make([]stamp, len(r.watch))
for i, path := range r.watch {
st, err := os.Stat(path)
if err != nil {
continue
}
out[i] = stamp{mod: st.ModTime(), size: st.Size(), ok: true}
}
return out
}
func sameStamps(a, b []stamp) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i].ok != b[i].ok || a[i].size != b[i].size || !a[i].mod.Equal(b[i].mod) {
return false
}
}
return true
}
func loadPair(certFile string, keyFiles []string) (*tls.Certificate, error) {
var firstErr error
for _, keyFile := range keyFiles {
if keyFile == "" {
continue
}
pair, err := tls.LoadX509KeyPair(certFile, keyFile)
if err == nil {
return &pair, nil
}
if firstErr == nil {
firstErr = err
}
}
if firstErr == nil {
firstErr = errors.New("no key file")
}
return nil, fmt.Errorf("tlsreload: the certificate %s: %w", certFile, firstErr)
}