-
Notifications
You must be signed in to change notification settings - Fork 4.3k
/
Copy pathlogical.go
600 lines (520 loc) · 16.3 KB
/
logical.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
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
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package http
import (
"bufio"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"mime"
"net"
"net/http"
"strconv"
"strings"
"time"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/helper/consts"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/vault"
"go.uber.org/atomic"
)
// bufferedReader can be used to replace a request body with a buffered
// version. The Close method invokes the original Closer.
type bufferedReader struct {
*bufio.Reader
rOrig io.ReadCloser
}
func newBufferedReader(r io.ReadCloser) *bufferedReader {
return &bufferedReader{
Reader: bufio.NewReader(r),
rOrig: r,
}
}
func (b *bufferedReader) Close() error {
return b.rOrig.Close()
}
const MergePatchContentTypeHeader = "application/merge-patch+json"
func buildLogicalRequestNoAuth(perfStandby bool, ra *vault.RouterAccess, w http.ResponseWriter, r *http.Request) (*logical.Request, io.ReadCloser, int, error) {
ns, err := namespace.FromContext(r.Context())
if err != nil {
return nil, nil, http.StatusBadRequest, nil
}
path := trimPath(ns, r.URL.Path)
var data map[string]interface{}
var origBody io.ReadCloser
var passHTTPReq bool
var responseWriter http.ResponseWriter
// Determine the operation
var op logical.Operation
switch r.Method {
case "DELETE":
op = logical.DeleteOperation
data = parseQuery(r.URL.Query())
case "GET":
op = logical.ReadOperation
queryVals := r.URL.Query()
var list bool
var err error
listStr := queryVals.Get("list")
if listStr != "" {
list, err = strconv.ParseBool(listStr)
if err != nil {
return nil, nil, http.StatusBadRequest, nil
}
if list {
queryVals.Del("list")
op = logical.ListOperation
if !strings.HasSuffix(path, "/") {
path += "/"
}
}
}
data = parseQuery(queryVals)
switch {
case strings.HasPrefix(path, "sys/pprof/"):
passHTTPReq = true
responseWriter = w
case path == "sys/storage/raft/snapshot":
responseWriter = w
case path == "sys/internal/counters/activity/export":
responseWriter = w
case path == "sys/monitor":
passHTTPReq = true
responseWriter = w
}
case "POST", "PUT":
op = logical.UpdateOperation
// Buffer the request body in order to allow us to peek at the beginning
// without consuming it. This approach involves no copying.
bufferedBody := newBufferedReader(r.Body)
r.Body = bufferedBody
// If we are uploading a snapshot or receiving an ocsp-request (which
// is der encoded) we don't want to parse it. Instead, we will simply
// add the HTTP request to the logical request object for later consumption.
contentType := r.Header.Get("Content-Type")
if (ra != nil && ra.IsBinaryPath(r.Context(), path)) ||
path == "sys/storage/raft/snapshot" || path == "sys/storage/raft/snapshot-force" {
passHTTPReq = true
origBody = r.Body
} else {
// Sample the first bytes to determine whether this should be parsed as
// a form or as JSON. The amount to look ahead (512 bytes) is arbitrary
// but extremely tolerant (i.e. allowing 511 bytes of leading whitespace
// and an incorrect content-type).
head, err := bufferedBody.Peek(512)
if err != nil && err != bufio.ErrBufferFull && err != io.EOF {
status := http.StatusBadRequest
logical.AdjustErrorStatusCode(&status, err)
return nil, nil, status, fmt.Errorf("error reading data")
}
if isForm(head, contentType) {
formData, err := parseFormRequest(r)
if err != nil {
status := http.StatusBadRequest
logical.AdjustErrorStatusCode(&status, err)
return nil, nil, status, fmt.Errorf("error parsing form data")
}
data = formData
} else {
origBody, err = parseJSONRequest(perfStandby, r, w, &data)
if err == io.EOF {
data = nil
err = nil
}
if err != nil {
status := http.StatusBadRequest
logical.AdjustErrorStatusCode(&status, err)
return nil, nil, status, fmt.Errorf("error parsing JSON")
}
}
}
case "PATCH":
op = logical.PatchOperation
contentTypeHeader := r.Header.Get("Content-Type")
contentType, _, err := mime.ParseMediaType(contentTypeHeader)
if err != nil {
status := http.StatusBadRequest
logical.AdjustErrorStatusCode(&status, err)
return nil, nil, status, err
}
if contentType != MergePatchContentTypeHeader {
return nil, nil, http.StatusUnsupportedMediaType, fmt.Errorf("PATCH requires Content-Type of %s, provided %s", MergePatchContentTypeHeader, contentType)
}
origBody, err = parseJSONRequest(perfStandby, r, w, &data)
if err == io.EOF {
data = nil
err = nil
}
if err != nil {
status := http.StatusBadRequest
logical.AdjustErrorStatusCode(&status, err)
return nil, nil, status, fmt.Errorf("error parsing JSON")
}
case "LIST":
op = logical.ListOperation
if !strings.HasSuffix(path, "/") {
path += "/"
}
data = parseQuery(r.URL.Query())
case "HEAD":
op = logical.HeaderOperation
data = parseQuery(r.URL.Query())
case "OPTIONS":
default:
return nil, nil, http.StatusMethodNotAllowed, nil
}
// RFC 5785 Redirect, keep the request for auditing purposes
if r.URL.Path != r.RequestURI {
passHTTPReq = true
}
requestId, err := uuid.GenerateUUID()
if err != nil {
return nil, nil, http.StatusInternalServerError, fmt.Errorf("failed to generate identifier for the request: %w", err)
}
req := &logical.Request{
ID: requestId,
Operation: op,
Path: path,
Data: data,
Connection: getConnection(r),
Headers: r.Header,
}
if ra != nil && ra.IsLimitedPath(r.Context(), path) {
req.PathLimited = true
}
if passHTTPReq {
req.HTTPRequest = r
}
if responseWriter != nil {
req.ResponseWriter = logical.NewHTTPResponseWriter(responseWriter)
}
return req, origBody, 0, nil
}
func buildLogicalPath(r *http.Request) (string, int, error) {
ns, err := namespace.FromContext(r.Context())
if err != nil {
return "", http.StatusBadRequest, nil
}
path := ns.TrimmedPath(strings.TrimPrefix(r.URL.Path, "/v1/"))
switch r.Method {
case "GET":
var (
list bool
err error
)
queryVals := r.URL.Query()
listStr := queryVals.Get("list")
if listStr != "" {
list, err = strconv.ParseBool(listStr)
if err != nil {
return "", http.StatusBadRequest, nil
}
if list {
if !strings.HasSuffix(path, "/") {
path += "/"
}
}
}
case "LIST":
if !strings.HasSuffix(path, "/") {
path += "/"
}
}
return path, 0, nil
}
func buildLogicalRequest(core *vault.Core, w http.ResponseWriter, r *http.Request, chrootNamespace string) (*logical.Request, io.ReadCloser, int, error) {
req, origBody, status, err := buildLogicalRequestNoAuth(core.PerfStandby(), core.RouterAccess(), w, r)
if err != nil || status != 0 {
return nil, nil, status, err
}
req.ChrootNamespace = chrootNamespace
req.SetRequiredState(r.Header.Values(VaultIndexHeaderName))
requestAuth(r, req)
req, err = requestWrapInfo(r, req)
if err != nil {
return nil, nil, http.StatusBadRequest, fmt.Errorf("error parsing X-Vault-Wrap-TTL header: %w", err)
}
err = parseMFAHeader(req)
if err != nil {
return nil, nil, http.StatusBadRequest, fmt.Errorf("failed to parse X-Vault-MFA header: %w", err)
}
err = requestPolicyOverride(r, req)
if err != nil {
return nil, nil, http.StatusBadRequest, fmt.Errorf("failed to parse %s header: %w", PolicyOverrideHeaderName, err)
}
return req, origBody, 0, nil
}
// handleLogical returns a handler for processing logical requests. These requests
// may or may not end up getting forwarded under certain scenarios if the node
// is a performance standby. Some of these cases include:
// - Perf standby and token with limited use count.
// - Perf standby and token re-validation needed (e.g. due to invalid token).
// - Perf standby and control group error.
func handleLogical(core *vault.Core, chrootNamespace string) http.Handler {
return handleLogicalInternal(core, false, false, chrootNamespace)
}
// handleLogicalWithInjector returns a handler for processing logical requests
// that also have their logical response data injected at the top-level payload.
// All forwarding behavior remains the same as `handleLogical`.
func handleLogicalWithInjector(core *vault.Core, chrootNamespace string) http.Handler {
return handleLogicalInternal(core, true, false, chrootNamespace)
}
// handleLogicalNoForward returns a handler for processing logical local-only
// requests. These types of requests never forwarded, and return an
// `vault.ErrCannotForwardLocalOnly` error if attempted to do so.
func handleLogicalNoForward(core *vault.Core, chrootNamespace string) http.Handler {
return handleLogicalInternal(core, false, true, chrootNamespace)
}
func handleLogicalRecovery(raw *vault.RawBackend, token *atomic.String) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
req, _, statusCode, err := buildLogicalRequestNoAuth(false, nil, w, r)
if err != nil || statusCode != 0 {
respondError(w, statusCode, err)
return
}
reqToken := r.Header.Get(consts.AuthHeaderName)
if reqToken == "" || token.Load() == "" || reqToken != token.Load() {
respondError(w, http.StatusForbidden, nil)
return
}
resp, err := raw.HandleRequest(r.Context(), req)
if respondErrorCommon(w, req, resp, err) {
return
}
var httpResp *logical.HTTPResponse
if resp != nil {
httpResp = logical.LogicalResponseToHTTPResponse(resp)
httpResp.RequestID = req.ID
}
respondOk(w, httpResp)
})
}
// handleLogicalInternal is a common helper that returns a handler for
// processing logical requests. The behavior depends on the various boolean
// toggles. Refer to usage on functions for possible behaviors.
func handleLogicalInternal(core *vault.Core, injectDataIntoTopLevel bool, noForward bool, chrootNamespace string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
req, origBody, statusCode, err := buildLogicalRequest(core, w, r, chrootNamespace)
if err != nil || statusCode != 0 {
respondError(w, statusCode, err)
return
}
// Websockets need to be handled at HTTP layer instead of logical requests.
ns, err := namespace.FromContext(r.Context())
if err != nil {
respondError(w, http.StatusInternalServerError, err)
return
}
trimmedPath := trimPath(ns, r.URL.Path)
nsPath := ns.Path
if ns.ID == namespace.RootNamespaceID {
nsPath = ""
}
if websocketPaths.HasPath(trimmedPath) {
handler := entHandleEventsSubscribe(core, req)
if handler != nil {
handler.ServeHTTP(w, r)
return
}
}
handler := handleEntPaths(nsPath, core, r)
if handler != nil {
handler.ServeHTTP(w, r)
return
}
// Make the internal request. We attach the connection info
// as well in case this is an authentication request that requires
// it. Vault core handles stripping this if we need to. This also
// handles all error cases; if we hit respondLogical, the request is a
// success.
resp, ok, needsForward := request(core, w, r, req)
switch {
case errwrap.Contains(resp.Error(), consts.ErrOverloaded.Error()):
respondError(w, http.StatusServiceUnavailable, consts.ErrOverloaded)
return
case needsForward && noForward:
respondError(w, http.StatusBadRequest, vault.ErrCannotForwardLocalOnly)
return
case needsForward && !noForward:
if origBody != nil {
r.Body = origBody
}
forwardRequest(core, w, r)
return
case !ok:
// If not ok, we simply return. The call on request should have
// taken care of setting the appropriate response code and payload
// in this case.
return
default:
// Build and return the proper response if everything is fine.
respondLogical(core, w, r, req, resp, injectDataIntoTopLevel)
return
}
})
}
func respondLogical(core *vault.Core, w http.ResponseWriter, r *http.Request, req *logical.Request, resp *logical.Response, injectDataIntoTopLevel bool) {
var httpResp *logical.HTTPResponse
var ret interface{}
// If vault's core has already written to the response writer do not add any
// additional output. Headers have already been sent.
if req != nil && req.ResponseWriter != nil && req.ResponseWriter.Written() {
return
}
if resp != nil {
if resp.Redirect != "" {
// If we have a redirect, redirect! We use a 307 code
// because we don't actually know if its permanent.
http.Redirect(w, r, resp.Redirect, 307)
return
}
// Check if this is a raw response
if _, ok := resp.Data[logical.HTTPStatusCode]; ok {
respondRaw(w, r, resp)
return
}
if resp.WrapInfo != nil && resp.WrapInfo.Token != "" {
httpResp = &logical.HTTPResponse{
WrapInfo: &logical.HTTPWrapInfo{
Token: resp.WrapInfo.Token,
Accessor: resp.WrapInfo.Accessor,
TTL: int(resp.WrapInfo.TTL.Seconds()),
CreationTime: resp.WrapInfo.CreationTime.Format(time.RFC3339Nano),
CreationPath: resp.WrapInfo.CreationPath,
WrappedAccessor: resp.WrapInfo.WrappedAccessor,
},
}
} else {
httpResp = logical.LogicalResponseToHTTPResponse(resp)
httpResp.RequestID = req.ID
}
ret = httpResp
if injectDataIntoTopLevel {
injector := logical.HTTPSysInjector{
Response: httpResp,
}
ret = injector
}
}
entAdjustResponse(core, w, req)
// Respond
respondOk(w, ret)
return
}
// respondRaw is used when the response is using HTTPContentType and HTTPRawBody
// to change the default response handling. This is only used for specific things like
// returning the CRL information on the PKI backends.
func respondRaw(w http.ResponseWriter, r *http.Request, resp *logical.Response) {
retErr := func(w http.ResponseWriter, err string) {
w.Header().Set("X-Vault-Raw-Error", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write(nil)
}
// Ensure this is never a secret or auth response
if resp.Secret != nil || resp.Auth != nil {
retErr(w, "raw responses cannot contain secrets or auth")
return
}
// Get the status code
statusRaw, ok := resp.Data[logical.HTTPStatusCode]
if !ok {
retErr(w, "no status code given")
return
}
var status int
switch statusRaw.(type) {
case int:
status = statusRaw.(int)
case float64:
status = int(statusRaw.(float64))
case json.Number:
s64, err := statusRaw.(json.Number).Float64()
if err != nil {
retErr(w, "cannot decode status code")
return
}
status = int(s64)
default:
retErr(w, "cannot decode status code")
return
}
nonEmpty := status != http.StatusNoContent
var contentType string
var body []byte
// Get the content type header; don't require it if the body is empty
contentTypeRaw, ok := resp.Data[logical.HTTPContentType]
if !ok && nonEmpty {
retErr(w, "no content type given")
return
}
if ok {
contentType, ok = contentTypeRaw.(string)
if !ok {
retErr(w, "cannot decode content type")
return
}
}
if nonEmpty {
// Get the body
bodyRaw, ok := resp.Data[logical.HTTPRawBody]
if !ok {
goto WRITE_RESPONSE
}
switch bodyRaw.(type) {
case string:
// This is best effort. The value may already be base64-decoded so
// if it doesn't work we just use as-is
bodyDec, err := base64.StdEncoding.DecodeString(bodyRaw.(string))
if err == nil {
body = bodyDec
} else {
body = []byte(bodyRaw.(string))
}
case []byte:
body = bodyRaw.([]byte)
default:
retErr(w, "cannot decode body")
return
}
}
WRITE_RESPONSE:
// Write the response
if contentType != "" {
w.Header().Set("Content-Type", contentType)
}
if cacheControl, ok := resp.Data[logical.HTTPCacheControlHeader].(string); ok {
w.Header().Set("Cache-Control", cacheControl)
}
if pragma, ok := resp.Data[logical.HTTPPragmaHeader].(string); ok {
w.Header().Set("Pragma", pragma)
}
if wwwAuthn, ok := resp.Data[logical.HTTPWWWAuthenticateHeader].(string); ok {
w.Header().Set("WWW-Authenticate", wwwAuthn)
}
w.WriteHeader(status)
w.Write(body)
}
// getConnection is used to format the connection information for
// attaching to a logical request
func getConnection(r *http.Request) (connection *logical.Connection) {
var remoteAddr string
var remotePort int
remoteAddr, port, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
remoteAddr = ""
} else {
remotePort, err = strconv.Atoi(port)
if err != nil {
remotePort = 0
}
}
connection = &logical.Connection{
RemoteAddr: remoteAddr,
RemotePort: remotePort,
ConnState: r.TLS,
}
return
}