1 package notmain
2
3 import (
4 "context"
5 "flag"
6 "fmt"
7 "net/http"
8 "net/url"
9 "os"
10 "strings"
11 "time"
12
13 "github.com/prometheus/client_golang/prometheus"
14 "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
15
16 "github.com/letsencrypt/boulder/cmd"
17 "github.com/letsencrypt/boulder/config"
18 "github.com/letsencrypt/boulder/db"
19 "github.com/letsencrypt/boulder/features"
20 bgrpc "github.com/letsencrypt/boulder/grpc"
21 "github.com/letsencrypt/boulder/issuance"
22 blog "github.com/letsencrypt/boulder/log"
23 "github.com/letsencrypt/boulder/metrics/measured_http"
24 "github.com/letsencrypt/boulder/ocsp/responder"
25 "github.com/letsencrypt/boulder/ocsp/responder/live"
26 redis_responder "github.com/letsencrypt/boulder/ocsp/responder/redis"
27 rapb "github.com/letsencrypt/boulder/ra/proto"
28 rocsp_config "github.com/letsencrypt/boulder/rocsp/config"
29 "github.com/letsencrypt/boulder/sa"
30 sapb "github.com/letsencrypt/boulder/sa/proto"
31 )
32
33 type Config struct {
34 OCSPResponder struct {
35 DebugAddr string `validate:"hostname_port"`
36 DB cmd.DBConfig `validate:"required_without_all=Source SAService,structonly"`
37
38
39
40
41
42 Source string `validate:"required_without_all=DB.DBConnectFile SAService Redis"`
43
44
45
46 IssuerCerts []string `validate:"min=1,dive,required"`
47
48 Path string
49
50
51
52 ListenAddress string `validate:"omitempty,hostname_port"`
53
54
55
56 Timeout config.Duration `validate:"-"`
57
58
59
60 LiveSigningPeriod config.Duration `validate:"-"`
61
62
63
64
65
66
67
68
69 MaxInflightSignings int `validate:"min=0"`
70
71
72
73
74
75
76
77
78
79
80
81 MaxSigningWaiters int `validate:"min=0"`
82
83 ShutdownStopTimeout config.Duration
84
85 RequiredSerialPrefixes []string `validate:"omitempty,dive,hexadecimal"`
86
87 Features map[string]bool
88
89
90
91 Redis *rocsp_config.RedisConfig `validate:"required_without=Source"`
92
93
94 TLS cmd.TLSConfig `validate:"required_without=Source,structonly"`
95
96
97
98 RAService *cmd.GRPCClientConfig
99
100
101
102
103 SAService *cmd.GRPCClientConfig `validate:"required_without_all=DB.DBConnectFile Source"`
104
105
106
107
108 LogSampleRate int `validate:"min=0"`
109 }
110
111 Syslog cmd.SyslogConfig
112 OpenTelemetry cmd.OpenTelemetryConfig
113
114
115 OpenTelemetryHTTPConfig cmd.OpenTelemetryHTTPConfig
116 }
117
118 func main() {
119 configFile := flag.String("config", "", "File path to the configuration file for this service")
120 flag.Parse()
121
122 if *configFile == "" {
123 fmt.Fprintf(os.Stderr, `Usage of %s:
124 Config JSON should contain either a DBConnectFile or a Source value containing a file: URL.
125 If Source is a file: URL, the file should contain a list of OCSP responses in base64-encoded DER,
126 as generated by Boulder's ceremony command.
127 `, os.Args[0])
128 flag.PrintDefaults()
129 os.Exit(1)
130 }
131
132 var c Config
133 err := cmd.ReadConfigFile(*configFile, &c)
134 cmd.FailOnError(err, "Reading JSON config file into config structure")
135 err = features.Set(c.OCSPResponder.Features)
136 cmd.FailOnError(err, "Failed to set feature flags")
137
138 scope, logger, oTelShutdown := cmd.StatsAndLogging(c.Syslog, c.OpenTelemetry, c.OCSPResponder.DebugAddr)
139 logger.Info(cmd.VersionString())
140
141 clk := cmd.Clock()
142
143 var source responder.Source
144
145 if strings.HasPrefix(c.OCSPResponder.Source, "file:") {
146 url, err := url.Parse(c.OCSPResponder.Source)
147 cmd.FailOnError(err, "Source was not a URL")
148 filename := url.Path
149
150
151 if filename == "" {
152 filename = url.Opaque
153 }
154 source, err = responder.NewMemorySourceFromFile(filename, logger)
155 cmd.FailOnError(err, fmt.Sprintf("Couldn't read file: %s", url.Path))
156 } else {
157
158 rocspRWClient, err := rocsp_config.MakeClient(c.OCSPResponder.Redis, clk, scope)
159 cmd.FailOnError(err, "Could not make redis client")
160
161 err = rocspRWClient.Ping(context.Background())
162 cmd.FailOnError(err, "pinging Redis")
163
164 liveSigningPeriod := c.OCSPResponder.LiveSigningPeriod.Duration
165 if liveSigningPeriod == 0 {
166 liveSigningPeriod = 60 * time.Hour
167 }
168
169 tlsConfig, err := c.OCSPResponder.TLS.Load(scope)
170 cmd.FailOnError(err, "TLS config")
171
172 raConn, err := bgrpc.ClientSetup(c.OCSPResponder.RAService, tlsConfig, scope, clk)
173 cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to RA")
174 rac := rapb.NewRegistrationAuthorityClient(raConn)
175
176 maxInflight := c.OCSPResponder.MaxInflightSignings
177 if maxInflight == 0 {
178 maxInflight = 1000
179 }
180 liveSource := live.New(rac, int64(maxInflight), c.OCSPResponder.MaxSigningWaiters)
181
182 rocspSource, err := redis_responder.NewRedisSource(rocspRWClient, liveSource, liveSigningPeriod, clk, scope, logger, c.OCSPResponder.LogSampleRate)
183 cmd.FailOnError(err, "Could not create redis source")
184
185 var dbMap *db.WrappedMap
186 if c.OCSPResponder.DB != (cmd.DBConfig{}) {
187 dbMap, err = sa.InitWrappedDb(c.OCSPResponder.DB, scope, logger)
188 cmd.FailOnError(err, "While initializing dbMap")
189 }
190
191 var sac sapb.StorageAuthorityReadOnlyClient
192 if c.OCSPResponder.SAService != nil {
193 saConn, err := bgrpc.ClientSetup(c.OCSPResponder.SAService, tlsConfig, scope, clk)
194 cmd.FailOnError(err, "Failed to load credentials and create gRPC connection to SA")
195 sac = sapb.NewStorageAuthorityReadOnlyClient(saConn)
196 }
197
198 source, err = redis_responder.NewCheckedRedisSource(rocspSource, dbMap, sac, scope, logger)
199 cmd.FailOnError(err, "Could not create checkedRedis source")
200 }
201
202
203 issuerCerts := make([]*issuance.Certificate, len(c.OCSPResponder.IssuerCerts))
204 for i, issuerFile := range c.OCSPResponder.IssuerCerts {
205 issuerCert, err := issuance.LoadCertificate(issuerFile)
206 cmd.FailOnError(err, "Could not load issuer cert")
207 issuerCerts[i] = issuerCert
208 }
209
210 source, err = responder.NewFilterSource(
211 issuerCerts,
212 c.OCSPResponder.RequiredSerialPrefixes,
213 source,
214 scope,
215 logger,
216 clk,
217 )
218 cmd.FailOnError(err, "Could not create filtered source")
219
220 m := mux(c.OCSPResponder.Path, source, c.OCSPResponder.Timeout.Duration, scope, c.OpenTelemetryHTTPConfig.Options(), logger, c.OCSPResponder.LogSampleRate)
221
222 srv := &http.Server{
223 ReadTimeout: 30 * time.Second,
224 WriteTimeout: 120 * time.Second,
225 IdleTimeout: 120 * time.Second,
226 Addr: c.OCSPResponder.ListenAddress,
227 Handler: m,
228 }
229
230 err = srv.ListenAndServe()
231 if err != nil && err != http.ErrServerClosed {
232 cmd.FailOnError(err, "Running HTTP server")
233 }
234
235
236
237
238
239
240 defer func() {
241 ctx, cancel := context.WithTimeout(context.Background(),
242 c.OCSPResponder.ShutdownStopTimeout.Duration)
243 defer cancel()
244 _ = srv.Shutdown(ctx)
245 oTelShutdown(ctx)
246 }()
247
248 cmd.WaitForSignal()
249 }
250
251
252
253
254
255
256 type ocspMux struct {
257 handler http.Handler
258 }
259
260 func (om *ocspMux) Handler(_ *http.Request) (http.Handler, string) {
261 return om.handler, "/"
262 }
263
264 func mux(responderPath string, source responder.Source, timeout time.Duration, stats prometheus.Registerer, oTelHTTPOptions []otelhttp.Option, logger blog.Logger, sampleRate int) http.Handler {
265 stripPrefix := http.StripPrefix(responderPath, responder.NewResponder(source, timeout, stats, logger, sampleRate))
266 h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
267 if r.Method == "GET" && r.URL.Path == "/" {
268 w.Header().Set("Cache-Control", "max-age=43200")
269 w.WriteHeader(200)
270 return
271 }
272 stripPrefix.ServeHTTP(w, r)
273 })
274 return measured_http.New(&ocspMux{h}, cmd.Clock(), stats, oTelHTTPOptions...)
275 }
276
277 func init() {
278 cmd.RegisterCommand("ocsp-responder", main, &cmd.ConfigValidator{Config: &Config{}})
279 }
280
View as plain text