-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathoptions.go
More file actions
335 lines (268 loc) · 7.92 KB
/
Copy pathoptions.go
File metadata and controls
335 lines (268 loc) · 7.92 KB
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
package safehttp
import (
"fmt"
"net"
"net/netip"
"slices"
"strings"
"time"
)
// Option changes safehttp construction.
type Option func(*options) error
type options struct {
schemes []string
ports []uint16
hosts []string
origins []string
methods []string
maxRedirects int
allowCredentials bool
allowCustomHost bool
allowPrefixes []netip.Prefix
denyPrefixes []netip.Prefix
dialer *net.Dialer
clientTimeout time.Duration
maxResponseHeader int64
maxResponseBytes int64
explicitSchemes bool
explicitPorts bool
explicitHosts bool
explicitOrigins bool
}
// AllowSchemes replaces the default allowed URL schemes.
func AllowSchemes(schemes ...string) Option {
schemes = slices.Clone(schemes)
return func(o *options) error {
if o.explicitOrigins {
return fmt.Errorf("safehttp: AllowSchemes cannot be combined with AllowOrigins")
}
if len(schemes) == 0 {
return fmt.Errorf("safehttp: allowed schemes cannot be empty")
}
o.schemes = schemes
o.explicitSchemes = true
return nil
}
}
// AllowPorts replaces the default allowed destination ports.
func AllowPorts(ports ...uint16) Option {
ports = slices.Clone(ports)
return func(o *options) error {
if o.explicitOrigins {
return fmt.Errorf("safehttp: AllowPorts cannot be combined with AllowOrigins")
}
if len(ports) == 0 {
return fmt.Errorf("safehttp: allowed ports cannot be empty")
}
if slices.Contains(ports, 0) {
return fmt.Errorf("safehttp: allowed port cannot be 0")
}
o.ports = ports
o.explicitPorts = true
return nil
}
}
// AllowHosts restricts requests to exact hosts or leading wildcard patterns.
func AllowHosts(hosts ...string) Option {
hosts = slices.Clone(hosts)
return func(o *options) error {
if o.explicitOrigins {
return fmt.Errorf("safehttp: AllowHosts cannot be combined with AllowOrigins")
}
if len(hosts) == 0 {
return fmt.Errorf("safehttp: allowed hosts cannot be empty")
}
o.hosts = hosts
o.explicitHosts = true
return nil
}
}
// AllowOrigins restricts requests to exact URL origins.
//
// Origins are normalized as scheme, host, and effective port. Path, query, and
// fragment components are ignored. AllowOrigins is an exact tuple policy, so
// multiple origins do not create cross-product allowances.
//
// AllowOrigins cannot be combined with AllowSchemes, AllowPorts, or AllowHosts.
func AllowOrigins(origins ...string) Option {
origins = slices.Clone(origins)
return func(o *options) error {
if o.explicitSchemes || o.explicitPorts || o.explicitHosts {
return fmt.Errorf("safehttp: AllowOrigins cannot be combined with AllowSchemes, AllowPorts, or AllowHosts")
}
if len(origins) == 0 {
return fmt.Errorf("safehttp: allowed origins cannot be empty")
}
o.origins = origins
o.explicitOrigins = true
return nil
}
}
// AllowMethods restricts requests to the provided HTTP methods.
func AllowMethods(methods ...string) Option {
methods = slices.Clone(methods)
return func(o *options) error {
if len(methods) == 0 {
return fmt.Errorf("safehttp: allowed methods cannot be empty")
}
o.methods = methods
return nil
}
}
// MaxRedirects sets how many redirects a client may follow.
func MaxRedirects(n int) Option {
return func(o *options) error {
if n < 0 {
return fmt.Errorf("safehttp: max redirects cannot be negative")
}
o.maxRedirects = n
return nil
}
}
// NoRedirects blocks every redirect.
func NoRedirects() Option {
return MaxRedirects(0)
}
// AllowCredentials permits URL userinfo.
func AllowCredentials() Option {
return func(o *options) error {
o.allowCredentials = true
return nil
}
}
// AllowCustomHostHeader permits a Request.Host value that differs from the URL.
func AllowCustomHostHeader() Option {
return func(o *options) error {
o.allowCustomHost = true
return nil
}
}
// AllowPrefixes permits destination IP prefixes that the default policy blocks.
//
// Use it for tests, private infrastructure, or other non-public destinations.
// DenyPrefixes still wins when the same address is covered by both an allow
// rule and a deny rule.
func AllowPrefixes(prefixes ...netip.Prefix) Option {
prefixes = slices.Clone(prefixes)
return func(o *options) error {
if len(prefixes) == 0 {
return fmt.Errorf("safehttp: allow prefixes cannot be empty")
}
for _, prefix := range prefixes {
if !prefix.IsValid() {
return fmt.Errorf("safehttp: invalid allow prefix")
}
}
o.allowPrefixes = append(o.allowPrefixes, prefixes...)
return nil
}
}
// DenyPrefixes blocks additional destination IP prefixes.
//
// Deny rules are checked before allow rules. Use them to make the default
// public-destination policy stricter for an application.
func DenyPrefixes(prefixes ...netip.Prefix) Option {
prefixes = slices.Clone(prefixes)
return func(o *options) error {
if len(prefixes) == 0 {
return fmt.Errorf("safehttp: deny prefixes cannot be empty")
}
for _, prefix := range prefixes {
if !prefix.IsValid() {
return fmt.Errorf("safehttp: invalid deny prefix")
}
}
o.denyPrefixes = append(o.denyPrefixes, prefixes...)
return nil
}
}
// AllowCIDRs parses and permits destination CIDR ranges.
//
// This is the string form of AllowPrefixes for callers that load policy from
// text or environment-specific configuration.
func AllowCIDRs(cidrs ...string) Option {
cidrs = slices.Clone(cidrs)
return func(o *options) error {
if len(cidrs) == 0 {
return fmt.Errorf("safehttp: allow cidrs cannot be empty")
}
for _, cidr := range cidrs {
prefix, err := netip.ParsePrefix(strings.TrimSpace(cidr))
if err != nil {
return fmt.Errorf("safehttp: invalid allow cidr %q: %w", cidr, err)
}
o.allowPrefixes = append(o.allowPrefixes, prefix)
}
return nil
}
}
// DenyCIDRs parses and blocks additional destination CIDR ranges.
//
// This is the string form of DenyPrefixes for callers that load policy from
// text or environment-specific configuration.
func DenyCIDRs(cidrs ...string) Option {
cidrs = slices.Clone(cidrs)
return func(o *options) error {
if len(cidrs) == 0 {
return fmt.Errorf("safehttp: deny cidrs cannot be empty")
}
for _, cidr := range cidrs {
prefix, err := netip.ParsePrefix(strings.TrimSpace(cidr))
if err != nil {
return fmt.Errorf("safehttp: invalid deny cidr %q: %w", cidr, err)
}
o.denyPrefixes = append(o.denyPrefixes, prefix)
}
return nil
}
}
// Dialer uses a copy of dialer and installs safehttp's control hook on the copy.
//
// Existing Control or ControlContext hooks are rejected because replacing them
// is how safehttp enforces the post-DNS address policy.
func Dialer(dialer *net.Dialer) Option {
return func(o *options) error {
if dialer == nil {
return fmt.Errorf("safehttp: dialer cannot be nil")
}
if dialer.Control != nil || dialer.ControlContext != nil {
return fmt.Errorf("safehttp: dialer control hooks are owned by safehttp")
}
copied := *dialer
o.dialer = &copied
return nil
}
}
// ClientTimeout sets http.Client.Timeout on clients built by NewClient.
func ClientTimeout(timeout time.Duration) Option {
return func(o *options) error {
if timeout < 0 {
return fmt.Errorf("safehttp: client timeout cannot be negative")
}
o.clientTimeout = timeout
return nil
}
}
// MaxResponseHeaderBytes sets http.Transport.MaxResponseHeaderBytes.
func MaxResponseHeaderBytes(n int64) Option {
return func(o *options) error {
if n < 0 {
return fmt.Errorf("safehttp: max response header bytes cannot be negative")
}
o.maxResponseHeader = n
return nil
}
}
// MaxResponseBytes caps the number of response body bytes a caller may read.
//
// The limit is enforced while the caller reads the response body. safehttp does
// not buffer the body up front.
func MaxResponseBytes(n int64) Option {
return func(o *options) error {
if n < 0 {
return fmt.Errorf("safehttp: max response bytes cannot be negative")
}
o.maxResponseBytes = n
return nil
}
}