forked from quay/clair
-
Notifications
You must be signed in to change notification settings - Fork 0
/
server.go
168 lines (155 loc) · 5.81 KB
/
server.go
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
// Package httptransport contains the HTTP logic for implementing the Clair(v4)
// HTTP API v1.
package httptransport
import (
"context"
"fmt"
"net/http"
"time"
"github.com/quay/clair/config"
"github.com/quay/zlog"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel"
"golang.org/x/sync/semaphore"
"github.com/quay/clair/v4/indexer"
"github.com/quay/clair/v4/matcher"
"github.com/quay/clair/v4/notifier"
)
// These are the various endpoints of the v1 API.
const (
apiRoot = "/api/v1/"
indexerRoot = "/indexer"
matcherRoot = "/matcher"
notifierRoot = "/notifier"
internalRoot = apiRoot + "internal/"
IndexAPIPath = indexerRoot + apiRoot + "index_report"
IndexReportAPIPath = indexerRoot + apiRoot + "index_report/"
IndexStateAPIPath = indexerRoot + apiRoot + "index_state"
AffectedManifestAPIPath = indexerRoot + internalRoot + "affected_manifest/"
VulnerabilityReportPath = matcherRoot + apiRoot + "vulnerability_report/"
UpdateOperationAPIPath = matcherRoot + internalRoot + "update_operation"
UpdateOperationDeleteAPIPath = matcherRoot + internalRoot + "update_operation/"
UpdateDiffAPIPath = matcherRoot + internalRoot + "update_diff"
NotificationAPIPath = notifierRoot + apiRoot + "notification/"
KeysAPIPath = notifierRoot + apiRoot + "services/notifier/keys"
KeyByIDAPIPath = notifierRoot + apiRoot + "services/notifier/keys/"
OpenAPIV1Path = "/openapi/v1"
)
// New configures an http.Handler serving the v1 API or a portion of it,
// according to the passed Config object.
func New(ctx context.Context, conf *config.Config, indexer indexer.Service, matcher matcher.Service, notifier notifier.Service) (http.Handler, error) {
mux := http.NewServeMux()
traceOpt := otelhttp.WithTracerProvider(otel.GetTracerProvider())
ctx = zlog.ContextWithValues(ctx, "component", "httptransport/New")
mux.Handle(OpenAPIV1Path, DiscoveryHandler(ctx, OpenAPIV1Path, traceOpt))
zlog.Info(ctx).Str("path", OpenAPIV1Path).Msg("openapi discovery configured")
// NOTE(hank) My brain always wants to rewrite constructions like the
// following as a switch, but this is actually cleaner as an "if" sequence.
if conf.Mode == config.IndexerMode || conf.Mode == config.ComboMode {
if indexer == nil {
return nil, fmt.Errorf("mode %q requires an indexer service", conf.Mode)
}
prefix := indexerRoot + apiRoot
v1, err := NewIndexerV1(ctx, prefix, indexer, traceOpt)
if err != nil {
return nil, fmt.Errorf("indexer configuration: %w", err)
}
var sem *semaphore.Weighted
if ct := conf.Indexer.IndexReportRequestConcurrency; ct > 0 {
sem = semaphore.NewWeighted(int64(ct))
}
rl := &limitHandler{
Check: func(r *http.Request) (*semaphore.Weighted, string) {
if r.Method != http.MethodPost && r.URL.Path != IndexAPIPath {
return nil, ""
}
// Nil if the relevant config option isn't set.
return sem, IndexAPIPath
},
Next: v1,
}
mux.Handle(prefix, rl)
}
if conf.Mode == config.MatcherMode || conf.Mode == config.ComboMode {
if indexer == nil || matcher == nil {
return nil, fmt.Errorf("mode %q requires both an indexer service and a matcher service", conf.Mode)
}
prefix := matcherRoot + apiRoot
v1 := NewMatcherV1(ctx, prefix, matcher, indexer, time.Duration(conf.Matcher.CacheAge), traceOpt)
mux.Handle(prefix, v1)
}
if conf.Mode == config.NotifierMode || (conf.Mode == config.ComboMode && notifier != nil) {
if notifier == nil {
return nil, fmt.Errorf("mode %q requires a notifier service", conf.Mode)
}
prefix := notifierRoot + apiRoot
v1, err := NewNotificationV1(ctx, prefix, notifier, traceOpt)
if err != nil {
return nil, fmt.Errorf("notifier configuration: %w", err)
}
mux.Handle(prefix, v1)
}
if conf.Mode == config.ComboMode && notifier == nil {
zlog.Debug(ctx).Msg("skipping unconfigured notifier")
}
// Add endpoint authentication if configured to add auth. Must happen after
// mux was configured for given mode.
if conf.Auth.Any() {
h, err := authHandler(conf, mux)
if err != nil {
zlog.Warn(ctx).
Err(err).
Msg("received error configuring auth middleware")
return nil, err
}
final := http.NewServeMux()
final.Handle("/robots.txt", robotsHandler)
final.Handle("/", h)
return final, nil
}
mux.Handle("/robots.txt", robotsHandler)
return mux, nil
}
// IntraserviceIssuer is the issuer that will be used if Clair is configured to
// mint its own JWTs.
const IntraserviceIssuer = `clair-intraservice`
// Unmodified determines whether to return a conditional response.
func unmodified(r *http.Request, v string) bool {
if vs, ok := r.Header["If-None-Match"]; ok {
for _, rv := range vs {
if rv == v {
return true
}
}
}
return false
}
// WriterError is a helper that closes over an error that may be returned after
// writing a response body starts.
//
// The normal error flow can't be used, because the HTTP status code will have
// been sent and some amount of body data may have been written.
//
// To use this, make sure an error variable is predeclared and the returned
// function is deferred:
//
// var err error
// defer writerError(w, &err)()
// _, err = fallibleWrite(w)
func writerError(w http.ResponseWriter, e *error) func() {
const errHeader = `Clair-Error`
w.Header().Add("trailer", errHeader)
return func() {
if *e == nil {
return
}
w.Header().Add(errHeader, (*e).Error())
}
}
// SetCacheControl sets the "Cache-Control" header on the response.
func setCacheControl(w http.ResponseWriter, age time.Duration) {
// The odd format string means "print float as wide as needed and to 0
// precision."
const f = `max-age=%.f`
w.Header().Set("cache-control", fmt.Sprintf(f, age.Seconds()))
}