• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

couchbase / sync_gateway / 509

23 Jul 2025 05:48PM UTC coverage: 64.958% (+0.05%) from 64.911%
509

push

jenkins

web-flow
CBG-4754: move silent handler to trace level (#7644)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

15 of 16 new or added lines in 2 files covered. (93.75%)

9 existing lines in 3 files now uncovered.

39925 of 61463 relevant lines covered (64.96%)

0.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.2
/rest/handler.go
1
///  Copyright 2012-Present Couchbase, Inc.
2
//
3
//  Use of this software is governed by the Business Source License included
4
//  in the file licenses/BSL-Couchbase.txt.  As of the Change Date specified
5
//  in that file, in accordance with the Business Source License, use of this
6
//  software will be governed by the Apache License, Version 2.0, included in
7
//  the file licenses/APL2.txt.
8

9
package rest
10

11
import (
12
        "bytes"
13
        "compress/gzip"
14
        "context"
15
        "encoding/base64"
16
        "encoding/json"
17
        "fmt"
18
        "io"
19
        "math"
20
        "mime"
21
        "mime/multipart"
22
        "net/http"
23
        "net/url"
24
        "os"
25
        "regexp"
26
        "strconv"
27
        "strings"
28
        "sync/atomic"
29
        "time"
30

31
        "github.com/couchbase/sync_gateway/auth"
32
        "github.com/couchbase/sync_gateway/base"
33
        "github.com/couchbase/sync_gateway/db"
34
        "github.com/gorilla/mux"
35
        "github.com/pkg/errors"
36
)
37

38
const (
39
        minCompressibleJSONSize      = 1000
40
        sgcollectTokenInvalidRequest = "sgcollect token auth present in non-sgcollect request"
41
)
42

43
var _ http.Flusher = &CountedResponseWriter{}
44
var _ http.Flusher = &NonCountedResponseWriter{}
45
var _ http.Flusher = &EncodedResponseWriter{}
46

47
var _ http.Hijacker = &CountedResponseWriter{}
48
var _ http.Hijacker = &NonCountedResponseWriter{}
49

50
var ErrInvalidLogin = base.HTTPErrorf(http.StatusUnauthorized, "Invalid login")
51
var ErrLoginRequired = base.HTTPErrorf(http.StatusUnauthorized, "Login required")
52

53
// If set to true, JSON output will be pretty-printed.
54
var PrettyPrint bool = false
55

56
// If set to true, diagnostic data will be dumped if there's a problem with MIME multipart data
57
var DebugMultipart bool = false
58

59
var lastSerialNum uint64 = 0
60

61
func init() {
1✔
62
        DebugMultipart = (os.Getenv("GatewayDebugMultipart") != "")
1✔
63
}
1✔
64

65
var kNotFoundError = base.HTTPErrorf(http.StatusNotFound, "missing")
66

67
var wwwAuthenticateHeader = `Basic realm="` + base.ProductNameString + `"`
68

69
// Admin API Auth Roles
70
type RouteRole struct {
71
        RoleName       string
72
        DatabaseScoped bool
73
}
74

75
const RoleBucketWildcard = "*"
76

77
var (
78
        MobileSyncGatewayRole = RouteRole{"mobile_sync_gateway", true}
79
        BucketFullAccessRole  = RouteRole{"bucket_full_access", true}
80
        BucketAdmin           = RouteRole{"bucket_admin", true}
81
        FullAdminRole         = RouteRole{"admin", false}
82
        ClusterAdminRole      = RouteRole{"cluster_admin", false}
83
        ReadOnlyAdminRole     = RouteRole{"ro_admin", false}
84
)
85

86
var BucketScopedEndpointRoles = []RouteRole{MobileSyncGatewayRole, BucketFullAccessRole, BucketAdmin, FullAdminRole}
87
var ClusterScopedEndpointRolesRead = []RouteRole{ReadOnlyAdminRole, ClusterAdminRole, FullAdminRole}
88
var ClusterScopedEndpointRolesWrite = []RouteRole{ClusterAdminRole, FullAdminRole}
89

90
// Encapsulates the state of handling an HTTP request.
91
type handler struct {
92
        server                *ServerContext
93
        rq                    *http.Request
94
        response              CountableResponseWriter
95
        status                int
96
        statusMessage         string
97
        requestBody           *CountedRequestReader
98
        db                    *db.Database
99
        collection            *db.DatabaseCollectionWithUser
100
        user                  auth.User
101
        authorizedAdminUser   string
102
        privs                 handlerPrivs
103
        startTime             time.Time
104
        serialNumber          uint64
105
        formattedSerialNumber string
106
        loggedDuration        bool
107
        runOffline            bool       // allows running on an offline database
108
        allowNilDBContext     bool       // allow access to a database based only on name, looking up in metadata registry
109
        queryValues           url.Values // Copy of results of rq.URL.Query()
110
        permissionsResults    map[string]bool
111
        authScopeFunc         authScopeFunc
112
        httpLogLevel          *base.LogLevel // if set, always log HTTP information at this level, instead of the default
113
        rqCtx                 context.Context
114
        serverType            serverType
115
        sgcollect             bool // is called by sgcollect
116
}
117

118
type authScopeFunc func(context.Context, *handler) (string, error)
119

120
type handlerPrivs int
121

122
const (
123
        regularPrivs = iota // Handler requires valid authentication
124
        publicPrivs         // Handler Handler checks auth and falls back to guest if invalid or missing
125
        adminPrivs          // Handler ignores auth, always runs with root/admin privs
126
        metricsPrivs
127
)
128

129
// silentRequestLogLevel is the log level used for handlers that are silent, meaning they only log at trace level
130
const silentRequestLogLevel = base.LevelTrace
131

132
type handlerMethod func(*handler) error
133

134
// makeHandlerWithOptions creates an http.Handler that will run a handler with the given method handlerOptions
135
func makeHandlerWithOptions(server *ServerContext, privs handlerPrivs, accessPermissions []Permission, responsePermissions []Permission, method handlerMethod, options handlerOptions) http.Handler {
1✔
136
        return http.HandlerFunc(func(r http.ResponseWriter, rq *http.Request) {
2✔
137
                serverType := getCtxServerType(rq.Context())
1✔
138
                h := newHandler(server, privs, serverType, r, rq, options)
1✔
139
                err := h.invoke(method, accessPermissions, responsePermissions)
1✔
140
                h.writeError(err)
1✔
141
                if !options.skipLogDuration {
2✔
142
                        h.logDuration(true)
1✔
143
                }
1✔
144
                h.reportDbStats()
1✔
145
        })
146
}
147

148
// makeHandler creates an http.Handler that will run a handler with the given method
149
func makeHandler(server *ServerContext, privs handlerPrivs, accessPermissions []Permission, responsePermissions []Permission, method handlerMethod) http.Handler {
1✔
150
        return makeHandlerWithOptions(server, privs, accessPermissions, responsePermissions, method, handlerOptions{})
1✔
151
}
1✔
152

153
// makeSilentHandler creates an http.Handler that will run a handler with the given method only logging at debug
154
func makeSilentHandler(server *ServerContext, privs handlerPrivs, accessPermissions []Permission, responsePermissions []Permission, method handlerMethod) http.Handler {
1✔
155
        return makeHandlerWithOptions(server, privs, accessPermissions, responsePermissions, method, handlerOptions{
1✔
156
                httpLogLevel: base.Ptr(silentRequestLogLevel),
1✔
157
        })
1✔
158
}
1✔
159

160
// Creates an http.Handler that will run a handler with the given method even if the target DB is offline
161
func makeOfflineHandler(server *ServerContext, privs handlerPrivs, accessPermissions []Permission, responsePermissions []Permission, method handlerMethod) http.Handler {
1✔
162
        return makeHandlerWithOptions(server, privs, accessPermissions, responsePermissions, method, handlerOptions{
1✔
163
                runOffline: true,
1✔
164
        })
1✔
165
}
1✔
166

167
// makeMetadataDBOfflineHandler creates an http.Handler that will run a handler with the given method even if the target DB couldn't be instantiated
168
func makeMetadataDBOfflineHandler(server *ServerContext, privs handlerPrivs, accessPermissions []Permission, responsePermissions []Permission, method handlerMethod) http.Handler {
1✔
169
        return makeHandlerWithOptions(server, privs, accessPermissions, responsePermissions, method, handlerOptions{
1✔
170
                runOffline:        true,
1✔
171
                allowNilDBContext: true,
1✔
172
                skipLogDuration:   true,
1✔
173
        })
1✔
174
}
1✔
175

176
// makeHandlerSpecificAuthScope creates an http.Handler that will run a handler with the given method. It also takes in a callback function which when
177
// given the endpoint payload returns an auth scope.
178
func makeHandlerSpecificAuthScope(server *ServerContext, privs handlerPrivs, accessPermissions []Permission, responsePermissions []Permission, method handlerMethod, authScopeFunc authScopeFunc) http.Handler {
1✔
179
        return makeHandlerWithOptions(server, privs, accessPermissions, responsePermissions, method, handlerOptions{
1✔
180
                authScopeFunc: authScopeFunc,
1✔
181
        })
1✔
182
}
1✔
183

184
// handlerOptions defines behaviour for a given handler's initialization and invocation
185
type handlerOptions struct {
186
        runOffline        bool           // if true, allow handler to run when a database is offline
187
        allowNilDBContext bool           // if true, allow a db-scoped handler to be invoked with a nil dbContext in cases where the database config exists but has an error preventing dbContext initialization"
188
        skipLogDuration   bool           // if true, will skip logging HTTP response status/duration
189
        authScopeFunc     authScopeFunc  // if set, this callback function will be used to set the auth scope for a given request body
190
        httpLogLevel      *base.LogLevel // if set, log HTTP requests to this handler at this level, instead of the usual info level
191
        sgcollect         bool           // if true, this handler is being invoked as part of sgcollect
192
}
193

194
func newHandler(server *ServerContext, privs handlerPrivs, serverType serverType, r http.ResponseWriter, rq *http.Request, options handlerOptions) *handler {
1✔
195
        h := &handler{
1✔
196
                server:            server,
1✔
197
                privs:             privs,
1✔
198
                rq:                rq,
1✔
199
                response:          NewNonCountedResponseWriter(r),
1✔
200
                status:            http.StatusOK,
1✔
201
                serialNumber:      atomic.AddUint64(&lastSerialNum, 1),
1✔
202
                startTime:         time.Now(),
1✔
203
                runOffline:        options.runOffline,
1✔
204
                allowNilDBContext: options.allowNilDBContext,
1✔
205
                authScopeFunc:     options.authScopeFunc,
1✔
206
                httpLogLevel:      options.httpLogLevel,
1✔
207
                serverType:        serverType,
1✔
208
                sgcollect:         options.sgcollect,
1✔
209
        }
1✔
210

1✔
211
        // initialize h.rqCtx
1✔
212
        _ = h.ctx()
1✔
213

1✔
214
        return h
1✔
215
}
1✔
216

217
// ctx returns the request-scoped context for logging/cancellation.
218
func (h *handler) ctx() context.Context {
1✔
219
        if h.rqCtx == nil {
2✔
220
                serverAddr, err := h.getServerAddr()
1✔
221
                if err != nil {
1✔
222
                        base.AssertfCtx(h.rq.Context(), "Error getting server address: %v", err)
×
223
                }
×
224
                ctx := base.RequestLogCtx(h.rq.Context(), base.RequestData{
1✔
225
                        CorrelationID:     h.formatSerialNumber(),
1✔
226
                        RequestHost:       serverAddr,
1✔
227
                        RequestRemoteAddr: h.rq.RemoteAddr,
1✔
228
                })
1✔
229
                h.rqCtx = ctx
1✔
230
        }
231
        return h.rqCtx
1✔
232
}
233

234
func (h *handler) getServerAddr() (string, error) {
1✔
235
        return h.server.getServerAddr(h.serverType)
1✔
236
}
1✔
237

238
func (h *handler) addDatabaseLogContext(dbName string, logConfig *base.DbLogConfig) {
1✔
239
        if dbName != "" {
2✔
240
                h.rqCtx = base.DatabaseLogCtx(h.ctx(), dbName, logConfig)
1✔
241
        }
1✔
242
}
243

244
func (h *handler) addCollectionLogContext(scopeName, collectionName string) {
1✔
245
        if scopeName != "" && collectionName != "" {
2✔
246
                h.rqCtx = base.CollectionLogCtx(h.ctx(), scopeName, collectionName)
1✔
247
        }
1✔
248
}
249

250
// ParseKeyspace will return a db, scope and collection for a given '.' separated keyspace string.
251
// Returns nil for scope and/or collection if not present in the keyspace string.
252
func ParseKeyspace(ks string) (db string, scope, collection *string, err error) {
1✔
253
        parts := strings.Split(ks, base.ScopeCollectionSeparator)
1✔
254
        switch len(parts) {
1✔
255
        case 1:
1✔
256
                db = parts[0]
1✔
257
        case 2:
1✔
258
                db = parts[0]
1✔
259
                collection = &parts[1]
1✔
260
        case 3:
1✔
261
                db = parts[0]
1✔
262
                scope = &parts[1]
1✔
263
                collection = &parts[2]
1✔
264
        default:
1✔
265
                return "", nil, nil, fmt.Errorf("unknown keyspace format: %q - expected 1-3 fields", ks)
1✔
266
        }
267

268
        // make sure all declared fields have stuff in them
269
        if db == "" ||
1✔
270
                collection != nil && *collection == "" ||
1✔
271
                scope != nil && *scope == "" {
2✔
272
                return "", nil, nil, fmt.Errorf("keyspace fields cannot be empty: %q", ks)
1✔
273
        }
1✔
274

275
        return db, scope, collection, nil
1✔
276
}
277

278
// Top-level handler call. It's passed a pointer to the specific method to run.
279
func (h *handler) invoke(method handlerMethod, accessPermissions []Permission, responsePermissions []Permission) error {
1✔
280
        if h.server.Config.API.CompressResponses == nil || *h.server.Config.API.CompressResponses {
2✔
281
                var stat *base.SgwIntStat
1✔
282
                if h.shouldUpdateBytesTransferredStats() {
1✔
283
                        stat = h.db.DbStats.Database().PublicRestBytesWritten
×
284
                }
×
285
                if encoded := NewEncodedResponseWriter(h.response, h.rq, stat, defaultBytesStatsReportingInterval); encoded != nil {
2✔
286
                        h.response = encoded
1✔
287
                        defer encoded.Close()
1✔
288
                }
1✔
289
        }
290

291
        err := h.validateAndWriteHeaders(method, accessPermissions, responsePermissions)
1✔
292
        if err != nil {
2✔
293
                return err
1✔
294
        }
1✔
295

296
        return method(h) // Call the actual handler code
1✔
297
}
298

299
// shouldCheckAdminRBAC returns true if the request needs to check the server for permissions to run
300
func (h *handler) shouldCheckAdminRBAC() bool {
1✔
301
        sgcollectToken := h.server.SGCollect.getToken(h.rq.Header)
1✔
302
        if sgcollectToken != "" && h.sgcollect && h.server.SGCollect.hasValidToken(h.ctx(), sgcollectToken) {
2✔
303
                return false
1✔
304
        }
1✔
305
        if h.privs == adminPrivs && *h.server.Config.API.AdminInterfaceAuthentication {
2✔
306
                if sgcollectToken != "" && !h.sgcollect {
2✔
307
                        base.AssertfCtx(h.ctx(), sgcollectTokenInvalidRequest)
1✔
308
                }
1✔
309
                return true
1✔
310
        } else if h.privs == metricsPrivs && *h.server.Config.API.MetricsInterfaceAuthentication {
2✔
311
                return true
1✔
312
        }
1✔
313
        return false
1✔
314
}
315

316
// validateAndWriteHeaders sets up handler.db and validates the permission of the user and returns an error if there is not permission.
317
func (h *handler) validateAndWriteHeaders(method handlerMethod, accessPermissions []Permission, responsePermissions []Permission) (err error) {
1✔
318
        var isRequestLogged bool
1✔
319
        defer func() {
2✔
320
                if !isRequestLogged {
2✔
321
                        h.logRequestLine()
1✔
322
                }
1✔
323
                h.logRESTCount()
1✔
324

1✔
325
                // Perform request logging unless it's a silent endpoint
1✔
326
                if h.httpLogLevel == nil {
2✔
327
                        auditFields := base.AuditFields{
1✔
328
                                base.AuditFieldHTTPMethod: h.rq.Method,
1✔
329
                                base.AuditFieldHTTPPath:   h.rq.URL.Path,
1✔
330
                                base.AuditFieldHTTPStatus: h.status,
1✔
331
                        }
1✔
332
                        switch h.serverType {
1✔
333
                        case publicServer:
1✔
334
                                base.Audit(h.ctx(), base.AuditIDPublicHTTPAPIRequest, auditFields)
1✔
335
                        case adminServer:
1✔
336
                                base.Audit(h.ctx(), base.AuditIDAdminHTTPAPIRequest, auditFields)
1✔
337
                        case metricsServer:
×
338
                                base.Audit(h.ctx(), base.AuditIDMetricsHTTPAPIRequest, auditFields)
×
339
                        }
340
                }
341
        }()
342

343
        defer func() {
2✔
344
                // Now that we know the DB, add CORS headers to the response:
1✔
345
                if h.privs != adminPrivs && h.privs != metricsPrivs {
2✔
346
                        cors := h.server.Config.API.CORS
1✔
347
                        if h.db != nil {
2✔
348
                                cors = h.db.CORS
1✔
349
                        }
1✔
350
                        if cors != nil {
2✔
351
                                cors.AddResponseHeaders(h.rq, h.response)
1✔
352
                        }
1✔
353
                }
354
        }()
355

356
        if h.server.Config.Unsupported.AuditInfoProvider != nil && h.server.Config.Unsupported.AuditInfoProvider.RequestInfoHeaderName != nil {
1✔
357
                auditLoggingFields := h.rq.Header.Get(*h.server.Config.Unsupported.AuditInfoProvider.RequestInfoHeaderName)
×
358
                if auditLoggingFields != "" {
×
359
                        var fields base.AuditFields
×
360
                        err := base.JSONUnmarshal([]byte(auditLoggingFields), &fields)
×
361
                        if err != nil {
×
362
                                base.WarnfCtx(h.ctx(), "Error unmarshalling audit logging fields: %v", err)
×
363
                        } else {
×
364
                                h.rqCtx = base.AuditLogCtx(h.ctx(), fields)
×
365
                        }
×
366
                }
367
        }
368

369
        if h.server.Config.Unsupported.EffectiveUserHeaderName != nil {
1✔
370
                effectiveUserFields := h.rq.Header.Get(*h.server.Config.Unsupported.EffectiveUserHeaderName)
×
371
                if effectiveUserFields != "" {
×
372
                        var fields base.EffectiveUserPair
×
373
                        err := base.JSONUnmarshal([]byte(effectiveUserFields), &fields)
×
374
                        if err != nil {
×
375
                                base.WarnfCtx(h.ctx(), "Error unmarshalling effective user header fields: %v", err)
×
376
                        } else {
×
377
                                h.rqCtx = base.EffectiveUserIDLogCtx(h.ctx(), fields.Domain, fields.UserID)
×
378
                        }
×
379
                }
380
        }
381

382
        switch h.rq.Header.Get("Content-Encoding") {
1✔
383
        case "":
1✔
384
                h.requestBody = NewReaderCounter(h.rq.Body)
1✔
385
        case "gzip":
1✔
386
                // gzip encoding will get approximate bytes read stat
1✔
387
                var err error
1✔
388
                h.requestBody = NewReaderCounter(nil)
1✔
389
                gzipRequestReader, err := gzip.NewReader(h.rq.Body)
1✔
390
                if err != nil {
1✔
391
                        return err
×
392
                }
×
393
                h.requestBody.reader = gzipRequestReader
1✔
394
                h.rq.Header.Del("Content-Encoding") // to prevent double decoding later on
1✔
395
        default:
×
396
                return base.HTTPErrorf(http.StatusUnsupportedMediaType, "Unsupported Content-Encoding; use gzip")
×
397
        }
398

399
        if h.shouldShowProductVersion() {
2✔
400
                h.setHeader("Server", base.VersionString)
1✔
401
        } else {
2✔
402
                h.setHeader("Server", base.ProductNameString)
1✔
403
        }
1✔
404

405
        // If an Admin Request and admin auth enabled or a metrics request with metrics auth enabled we need to check the
406
        // user credentials
407
        shouldCheckAdminAuth := h.shouldCheckAdminRBAC()
1✔
408

1✔
409
        // If admin/metrics endpoint but auth not enabled, set admin_noauth log ctx
1✔
410
        if !shouldCheckAdminAuth && (h.serverType == adminServer || h.serverType == metricsServer) {
2✔
411
                h.rqCtx = base.UserLogCtx(h.ctx(), base.UserSyncGatewayAdmin, base.UserDomainSyncGatewayAdmin, nil)
1✔
412
        }
1✔
413

414
        keyspaceDb := h.PathVar("db")
1✔
415
        var keyspaceScope, keyspaceCollection *string
1✔
416

1✔
417
        // If there is a "keyspace" path variable in the route, parse the keyspace:
1✔
418
        ks := h.PathVar("keyspace")
1✔
419
        explicitCollectionLogging := false
1✔
420
        if ks != "" {
2✔
421
                var err error
1✔
422
                keyspaceDb, keyspaceScope, keyspaceCollection, err = ParseKeyspace(ks)
1✔
423
                if err != nil {
1✔
424
                        return err
×
425
                }
×
426
                // If the collection is known, add it to the context before getting the db. If we do not know it, or don't know scope, we'll add this information later.
427
                if keyspaceCollection != nil {
2✔
428
                        explicitCollectionLogging = true
1✔
429
                        // /db.collectionName/foo is valid since Sync Gateway only has one scope
1✔
430
                        scope := ""
1✔
431
                        if keyspaceScope != nil {
2✔
432
                                scope = *keyspaceScope
1✔
433
                        }
1✔
434
                        h.addCollectionLogContext(scope, *keyspaceCollection)
1✔
435
                }
436
        }
437

438
        var dbContext *db.DatabaseContext
1✔
439

1✔
440
        var bucketName string
1✔
441

1✔
442
        // look up the database context:
1✔
443
        if keyspaceDb != "" {
2✔
444
                var err error
1✔
445
                if dbContext, err = h.server.GetActiveDatabase(keyspaceDb); err != nil {
2✔
446
                        h.addDatabaseLogContext(keyspaceDb, nil)
1✔
447
                        if err == base.ErrNotFound {
2✔
448
                                if shouldCheckAdminAuth {
1✔
449
                                        // Check if authenticated before attempting to get inactive database
×
450
                                        authorized, err := h.checkAdminAuthenticationOnly()
×
451
                                        if err != nil {
×
452
                                                return err
×
453
                                        }
×
454
                                        if !authorized {
×
455
                                                return ErrInvalidLogin
×
456
                                        }
×
457
                                }
458
                                var dbConfigFound bool
1✔
459
                                dbContext, dbConfigFound, err = h.server.GetInactiveDatabase(h.ctx(), keyspaceDb)
1✔
460
                                if err != nil {
2✔
461
                                        if httpError, ok := err.(*base.HTTPError); ok && httpError.Status == http.StatusNotFound {
2✔
462
                                                if shouldCheckAdminAuth && (!h.allowNilDBContext || !dbConfigFound) {
1✔
463
                                                        return base.HTTPErrorf(http.StatusForbidden, "")
×
464
                                                } else if h.privs == regularPrivs || h.privs == publicPrivs {
2✔
465
                                                        if !h.providedAuthCredentials() {
2✔
466

1✔
467
                                                                return ErrLoginRequired
1✔
468
                                                        }
1✔
469
                                                        return ErrInvalidLogin
1✔
470
                                                }
471
                                        }
472
                                        if !h.allowNilDBContext || !dbConfigFound {
2✔
473
                                                base.InfofCtx(h.ctx(), base.KeyHTTP, "Error trying to get db %s: %v", base.MD(keyspaceDb), err)
1✔
474
                                                return err
1✔
475
                                        }
1✔
476
                                        bucketName, _ = h.server.bucketNameFromDbName(h.ctx(), keyspaceDb)
×
477
                                }
478
                        } else {
1✔
479
                                return err
1✔
480
                        }
1✔
481
                }
482
        }
483

484
        // If this call is in the context of a DB make sure the DB is in a valid state
485
        if dbContext != nil {
2✔
486
                h.addDatabaseLogContext(keyspaceDb, dbContext.Options.LoggingConfig)
1✔
487
                if !h.runOffline {
2✔
488
                        // get a read lock on the dbContext
1✔
489
                        // When the lock is returned we know that the db state will not be changed by
1✔
490
                        // any other call
1✔
491

1✔
492
                        // defer releasing the dbContext until after the handler method returns, unless it's a blipsync request
1✔
493
                        if !h.isBlipSync() {
2✔
494
                                dbContext.AccessLock.RLock()
1✔
495
                                defer dbContext.AccessLock.RUnlock()
1✔
496
                        }
1✔
497

498
                        dbState := atomic.LoadUint32(&dbContext.State)
1✔
499

1✔
500
                        // if dbState == db.DBOnline, continue flow and invoke the handler method
1✔
501
                        if dbState == db.DBOffline {
2✔
502
                                // DB is offline, only handlers with runOffline true can run in this state
1✔
503
                                return base.HTTPErrorf(http.StatusServiceUnavailable, "DB is currently under maintenance")
1✔
504
                        } else if dbState != db.DBOnline {
2✔
505
                                // DB is in transition state, no calls will be accepted until it is Online or Offline state
×
506
                                return base.HTTPErrorf(http.StatusServiceUnavailable, "DB is %v - try again later", db.RunStateString[dbState])
×
507
                        }
×
508
                }
509
        }
510

511
        // Authenticate, if not on admin port:
512
        if h.privs != adminPrivs {
2✔
513
                var err error
1✔
514
                if err = h.checkPublicAuth(dbContext); err != nil {
2✔
515
                        // if auth fails we still need to record bytes read over the rest api for the stat, to do this we need to call GetBodyBytesCount to read
1✔
516
                        // the body to populate the bytes count on the CountedRequestReader struct
1✔
517
                        bytesCount := h.requestBody.GetBodyBytesCount()
1✔
518
                        if bytesCount > 0 {
2✔
519
                                dbContext.DbStats.DatabaseStats.PublicRestBytesRead.Add(bytesCount)
1✔
520
                        }
1✔
521
                        return err
1✔
522
                }
523
                if h.user != nil && h.user.Name() == "" && dbContext != nil && dbContext.IsGuestReadOnly() {
2✔
524
                        // Prevent read-only guest access to any endpoint requiring write permissions except
1✔
525
                        // blipsync.  Read-only guest handling for websocket replication (blipsync) is evaluated
1✔
526
                        // at the blip message level to support read-only pull replications.
1✔
527
                        if requiresWritePermission(accessPermissions) && !h.isBlipSync() {
2✔
528
                                return base.HTTPErrorf(http.StatusForbidden, auth.GuestUserReadOnly)
1✔
529
                        }
1✔
530
                }
531
        }
532

533
        // If the user has OIDC roles/channels configured, we need to check if the OIDC issuer they came from is still valid.
534
        // Note: checkPublicAuth already does this check if the user authenticates with a bearer token, but we still need to recheck
535
        // for users using session tokens / basic auth. However, updatePrincipal will be idempotent.
536
        if h.user != nil && h.user.JWTIssuer() != "" {
2✔
537
                updates := checkJWTIssuerStillValid(h.ctx(), dbContext, h.user)
1✔
538
                if updates != nil {
2✔
539
                        _, _, err := dbContext.UpdatePrincipal(h.ctx(), updates, true, true)
1✔
540
                        if err != nil {
1✔
541
                                return fmt.Errorf("failed to revoke stale OIDC roles/channels: %w", err)
×
542
                        }
×
543
                        // TODO: could avoid this extra fetch if UpdatePrincipal returned the new principal
544
                        h.user, err = dbContext.Authenticator(h.ctx()).GetUser(*updates.Name)
1✔
545
                        if err != nil {
1✔
546
                                return err
×
547
                        }
×
548
                }
549
        }
550

551
        if shouldCheckAdminAuth {
1✔
552
                // If server is walrus but auth is enabled we should just kick the user out as invalid as we have nothing to
×
553
                // validate credentials against
×
554
                if base.ServerIsWalrus(h.server.Config.Bootstrap.Server) {
×
555
                        return base.HTTPErrorf(http.StatusUnauthorized, "Authorization not possible with Walrus server. "+
×
556
                                "Either use Couchbase Server or disable admin auth by setting api.admin_interface_authentication and api.metrics_interface_authentication to false.")
×
557
                }
×
558

559
                username, password := h.getBasicAuth()
×
560
                if username == "" {
×
561
                        if dbContext == nil || dbContext.Options.SendWWWAuthenticateHeader == nil || *dbContext.Options.SendWWWAuthenticateHeader {
×
562
                                h.response.Header().Set("WWW-Authenticate", wwwAuthenticateHeader)
×
563
                        }
×
564
                        base.Audit(h.ctx(), base.AuditIDAdminUserAuthenticationFailed, base.AuditFields{base.AuditFieldUserName: username})
×
565
                        return ErrLoginRequired
×
566
                }
567

568
                var managementEndpoints []string
×
569
                var httpClient *http.Client
×
570
                var authScope string
×
571

×
572
                var err error
×
573
                if dbContext != nil {
×
574
                        managementEndpoints, httpClient, err = dbContext.ObtainManagementEndpointsAndHTTPClient(h.ctx())
×
575
                        authScope = dbContext.Bucket.GetName()
×
576
                } else {
×
577
                        managementEndpoints, httpClient, err = h.server.ObtainManagementEndpointsAndHTTPClient()
×
578
                        authScope = bucketName
×
579
                }
×
580
                if err != nil {
×
581
                        base.WarnfCtx(h.ctx(), "An error occurred whilst obtaining management endpoints: %v", err)
×
582
                        return base.HTTPErrorf(http.StatusInternalServerError, "")
×
583
                }
×
584
                if h.authScopeFunc != nil {
×
585
                        authScope, err = h.authScopeFunc(h.ctx(), h)
×
586
                        if err != nil {
×
587
                                return base.HTTPErrorf(http.StatusInternalServerError, "Unable to read body: %v", err)
×
588
                        }
×
589
                        if authScope == "" {
×
590
                                return base.HTTPErrorf(http.StatusBadRequest, "Unable to determine auth scope for endpoint")
×
591
                        }
×
592
                }
593

594
                permissions, statusCode, err := checkAdminAuth(h.ctx(), authScope, username, password, h.rq.Method, httpClient,
×
595
                        managementEndpoints, *h.server.Config.API.EnableAdminAuthenticationPermissionsCheck, accessPermissions,
×
596
                        responsePermissions)
×
597
                if err != nil {
×
598
                        base.WarnfCtx(h.ctx(), "An error occurred whilst checking whether a user was authorized: %v", err)
×
599
                        return base.HTTPErrorf(http.StatusInternalServerError, "")
×
600
                }
×
601

602
                if statusCode != http.StatusOK {
×
603
                        base.InfofCtx(h.ctx(), base.KeyAuth, "%s: User %s failed to auth as an admin statusCode: %d", h.formatSerialNumber(), base.UD(username), statusCode)
×
604
                        if statusCode == http.StatusUnauthorized {
×
605
                                base.Audit(h.ctx(), base.AuditIDAdminUserAuthenticationFailed, base.AuditFields{base.AuditFieldUserName: username})
×
606
                                return ErrInvalidLogin
×
607
                        }
×
608
                        base.Audit(h.ctx(), base.AuditIDAdminUserAuthorizationFailed, base.AuditFields{base.AuditFieldUserName: username})
×
609
                        return base.HTTPErrorf(statusCode, "")
×
610
                }
611
                // even though `checkAdminAuth` _can_ issue whoami to find user's roles, it doesn't always...
612
                // to reduce code complexity, we'll potentially be making this whoami request twice if we need it for audit filtering
613
                auditRoleNames := getCBUserRolesForAudit(dbContext, authScope, h.ctx(), httpClient, managementEndpoints, username, password)
×
614

×
615
                h.authorizedAdminUser = username
×
616
                h.permissionsResults = permissions
×
617
                h.rqCtx = base.UserLogCtx(h.ctx(), username, base.UserDomainCBServer, auditRoleNames)
×
618
                // query auditRoleNames above even if this is a silent request can need "real_userid" on a ctx. Example: /_expvar should not log AuditIDAdminUserAuthenticated but it should log AuditIDSyncGatewayStats
×
619
                if !h.isSilentRequest() {
×
620
                        base.Audit(h.ctx(), base.AuditIDAdminUserAuthenticated, base.AuditFields{})
×
621
                }
×
622
                base.DebugfCtx(h.ctx(), base.KeyAuth, "%s: User %s was successfully authorized as an admin", h.formatSerialNumber(), base.UD(username))
×
623
        } else {
1✔
624
                // If admin auth is not enabled we should set any responsePermissions to true so that any handlers checking for
1✔
625
                // these still pass
1✔
626
                h.permissionsResults = make(map[string]bool)
1✔
627
                for _, responsePermission := range responsePermissions {
2✔
628
                        h.permissionsResults[responsePermission.PermissionName] = true
1✔
629
                }
1✔
630
        }
631

632
        // Collection keyspace handling
633
        if ks != "" {
2✔
634
                ksNotFound := base.HTTPErrorf(http.StatusNotFound, "keyspace %s not found", ks)
1✔
635
                if dbContext.Scopes != nil {
2✔
636
                        // endpoint like /db/doc where this matches /db._default._default/
1✔
637
                        // the check whether the _default._default actually exists on this database is performed below
1✔
638
                        if keyspaceScope == nil && keyspaceCollection == nil {
2✔
639
                                keyspaceScope = base.Ptr(base.DefaultScope)
1✔
640
                                keyspaceCollection = base.Ptr(base.DefaultCollection)
1✔
641
                        }
1✔
642
                        // endpoint like /db.collectionName/doc where this matches /db.scopeName.collectionName/doc because there's a single scope defined
643
                        if keyspaceScope == nil {
2✔
644
                                if len(dbContext.Scopes) == 1 {
2✔
645
                                        for scopeName, _ := range dbContext.Scopes {
2✔
646
                                                keyspaceScope = base.Ptr(scopeName)
1✔
647
                                        }
1✔
648

649
                                } else {
×
650
                                        // There are multiple scopes on a DatabaseContext. This isn't allowed in Sync Gateway but if the feature becomes available, it will be ambiguous which scope to match.
×
651
                                        return ksNotFound
×
652
                                }
×
653
                        }
654
                        scope, foundScope := dbContext.Scopes[*keyspaceScope]
1✔
655
                        if !foundScope {
2✔
656
                                return ksNotFound
1✔
657
                        }
1✔
658

659
                        _, foundCollection := scope.Collections[*keyspaceCollection]
1✔
660
                        if !foundCollection {
2✔
661
                                return ksNotFound
1✔
662
                        }
1✔
663
                } else {
×
664
                        if keyspaceScope != nil && *keyspaceScope != base.DefaultScope || keyspaceCollection != nil && *keyspaceCollection != base.DefaultCollection {
×
665
                                // request tried specifying a named collection on a non-named collections database
×
666
                                return ksNotFound
×
667
                        }
×
668
                        // Set these for handlers that expect a scope/collection to be set, even if not using named collections.
669
                        keyspaceScope = base.Ptr(base.DefaultScope)
×
670
                        keyspaceCollection = base.Ptr(base.DefaultCollection)
×
671
                }
672
                // explicitCollectionLogging is true if the collection was explicitly set in the keyspace string. When it is used, the log context will add a col:collection in log lines. If it is implicit, in the case of /db/doc, col: is omitted from the log information, but retained for audit logging purposes.
673
                if explicitCollectionLogging {
2✔
674
                        // matches the following:
1✔
675
                        //  - /db.scopeName.collectionName/doc
1✔
676
                        //  - /db.collectionName/doc
1✔
677
                        //  - /db._default/doc
1✔
678
                        //  - /db._default._default/doc
1✔
679
                        h.addCollectionLogContext(*keyspaceScope, *keyspaceCollection)
1✔
680
                } else {
2✔
681
                        h.rqCtx = base.ImplicitDefaultCollectionLogCtx(h.ctx())
1✔
682
                }
1✔
683
        }
684

685
        h.logRequestLine()
1✔
686
        isRequestLogged = true
1✔
687

1✔
688
        // Now set the request's Database (i.e. context + user)
1✔
689
        if dbContext != nil {
2✔
690
                var err error
1✔
691
                h.db, err = db.GetDatabase(dbContext, h.user)
1✔
692

1✔
693
                if err != nil {
1✔
694
                        return err
×
695
                }
×
696
                if ks != "" {
2✔
697
                        var err error
1✔
698
                        h.collection, err = h.db.GetDatabaseCollectionWithUser(*keyspaceScope, *keyspaceCollection)
1✔
699
                        if err != nil {
1✔
700
                                return err
×
701
                        }
×
702
                }
703
        }
704
        h.updateResponseWriter()
1✔
705
        // ensure wrapped ResponseWriter implements http.Flusher
1✔
706
        _, ok := h.response.(http.Flusher)
1✔
707
        if !ok {
1✔
708
                return fmt.Errorf("http.ResponseWriter %T does not implement Flusher interface", h.response)
×
709
        }
×
710
        return nil
1✔
711
}
712

713
func getCBUserRolesForAudit(db *db.DatabaseContext, authScope string, ctx context.Context, httpClient *http.Client, managementEndpoints []string, username, password string) []string {
×
714
        if !needRolesForAudit(db, base.UserDomainCBServer) {
×
715
                return nil
×
716
        }
×
717

718
        whoAmIResponse, whoAmIStatus, whoAmIErr := cbRBACWhoAmI(ctx, httpClient, managementEndpoints, username, password)
×
719
        if whoAmIErr != nil || whoAmIStatus != http.StatusOK {
×
720
                base.WarnfCtx(ctx, "An error occurred whilst fetching the user's roles for audit filtering - will not filter: %v", whoAmIErr)
×
721
                return nil
×
722
        }
×
723

724
        var auditRoleNames []string
×
725
        for _, role := range whoAmIResponse.Roles {
×
726
                // only filter roles applied to this bucket or cluster-scope
×
727
                if role.BucketName == "" || role.BucketName == authScope {
×
728
                        auditRoleNames = append(auditRoleNames, role.RoleName)
×
729
                }
×
730
        }
731
        return auditRoleNames
×
732
}
733

734
// removeCorruptConfigIfExists will remove the config from the bucket and remove it from the map if it exists on the invalid database config map
735
func (h *handler) removeCorruptConfigIfExists(ctx context.Context, bucket, configGroupID, dbName string) error {
1✔
736
        _, ok := h.server.invalidDatabaseConfigTracking.exists(dbName)
1✔
737
        if !ok {
2✔
738
                // exit early of it doesn't exist
1✔
739
                return nil
1✔
740
        }
1✔
741
        // remove the bad config from the bucket
742
        err := h.server.BootstrapContext.DeleteConfig(ctx, bucket, configGroupID, dbName)
1✔
743
        if err != nil && !base.IsDocNotFoundError(err) {
1✔
744
                return err
×
745
        }
×
746
        // delete the database name form the invalid database map on server context
747
        h.server.invalidDatabaseConfigTracking.remove(dbName)
1✔
748

1✔
749
        return nil
1✔
750
}
751

752
// isSilentRequest returns true if the handler represents a request we should suppress http logging on.
753
func (h *handler) isSilentRequest() bool {
×
NEW
754
        return h.httpLogLevel != nil && *h.httpLogLevel == silentRequestLogLevel
×
755
}
×
756

757
func (h *handler) logRequestLine() {
1✔
758

1✔
759
        logLevel := base.LevelInfo
1✔
760
        if h.httpLogLevel != nil {
2✔
761
                logLevel = *h.httpLogLevel
1✔
762
        }
1✔
763

764
        // Check Log Level first, as SanitizeRequestURL is expensive to evaluate.
765
        if !base.LogLevelEnabled(h.ctx(), logLevel, base.KeyHTTP) {
2✔
766
                return
1✔
767
        }
1✔
768

769
        proto := ""
1✔
770
        if h.rq.ProtoMajor >= 2 {
1✔
771
                proto = " HTTP/2"
×
772
        }
×
773

774
        queryValues := h.getQueryValues()
1✔
775

1✔
776
        base.LogLevelCtx(h.ctx(), logLevel, base.KeyHTTP, "%s %s%s%s", h.rq.Method, base.SanitizeRequestURL(h.rq, &queryValues), proto, h.formattedEffectiveUserName())
1✔
777
}
778

779
// shouldUpdateBytesTransferredStats returns true if we want to log the bytes transferred. The criteria is if this is db scoped over the public port. Blip is skipped since those stats are tracked separately.
780
func (h *handler) shouldUpdateBytesTransferredStats() bool {
1✔
781
        if h.db == nil {
2✔
782
                return false
1✔
783
        }
1✔
784
        if h.serverType != publicServer {
2✔
785
                return false
1✔
786
        }
1✔
787
        if h.isBlipSync() {
2✔
788
                return false
1✔
789
        }
1✔
790
        return true
1✔
791
}
792

793
// updateResponseWriter will create an updated Response Writer if we need to log db stats.
794
func (h *handler) updateResponseWriter() {
1✔
795
        if !h.shouldUpdateBytesTransferredStats() {
2✔
796
                return
1✔
797
        }
1✔
798
        h.response = NewCountedResponseWriter(h.response,
1✔
799
                h.db.DbStats.Database().PublicRestBytesWritten,
1✔
800
                defaultBytesStatsReportingInterval)
1✔
801
}
802

803
func (h *handler) logDuration(realTime bool) {
1✔
804
        if h.loggedDuration {
2✔
805
                return
1✔
806
        }
1✔
807
        h.loggedDuration = true
1✔
808

1✔
809
        var duration time.Duration
1✔
810
        if realTime {
2✔
811
                duration = time.Since(h.startTime)
1✔
812
        }
1✔
813

814
        // Log timings/status codes for errors under the HTTP log key
815
        // and the HTTPResp log key for everything else.
816
        logKey := base.KeyHTTPResp
1✔
817
        logLevel := base.LevelInfo
1✔
818

1✔
819
        // if error status, log at HTTP/Info
1✔
820
        if h.status >= 300 {
2✔
821
                logKey = base.KeyHTTP
1✔
822
        } else if h.httpLogLevel != nil {
3✔
823
                // if the handler requested a different log level, and the request was successful, we'll honour that log level.
1✔
824
                logLevel = *h.httpLogLevel
1✔
825
        }
1✔
826

827
        base.LogLevelCtx(h.ctx(), logLevel, logKey, "%s:     --> %d %s  (%.1f ms)",
1✔
828
                h.formatSerialNumber(), h.status, h.statusMessage,
1✔
829
                float64(duration)/float64(time.Millisecond),
1✔
830
        )
1✔
831
}
832

833
// logRESTCount will increment the number of public REST requests stat for public REST requests excluding _blipsync requests if they are attached to a database.
834
func (h *handler) logRESTCount() {
1✔
835
        if h.db == nil {
2✔
836
                return
1✔
837
        }
1✔
838
        if h.serverType != publicServer {
2✔
839
                return
1✔
840
        }
1✔
841
        if h.isBlipSync() {
2✔
842
                return
1✔
843
        }
1✔
844
        h.db.DbStats.DatabaseStats.NumPublicRestRequests.Add(1)
1✔
845
}
846

847
// reportDbStats will report the public rest request specific stats back to the database
848
func (h *handler) reportDbStats() {
1✔
849
        if !h.shouldUpdateBytesTransferredStats() {
2✔
850
                return
1✔
851
        }
1✔
852
        var bytesReadStat int64
1✔
853
        h.response.reportStats(true)
1✔
854
        // load the number of bytes read on the request
1✔
855
        bytesReadStat = h.requestBody.GetBodyBytesCount()
1✔
856
        // as this is int64 lets protect against a situation where a negative is returned from GetBodyBytesCount() function and thus decrementing the stat
1✔
857
        if bytesReadStat > 0 {
2✔
858
                h.db.DbStats.DatabaseStats.PublicRestBytesRead.Add(bytesReadStat)
1✔
859
        }
1✔
860
}
861

862
// logStatusWithDuration will log the request status and the duration of the request.
863
func (h *handler) logStatusWithDuration(status int, message string) {
1✔
864
        h.setStatus(status, message)
1✔
865
        h.logDuration(true)
1✔
866
}
1✔
867

868
// logStatus will log the request status, but NOT the duration of the request.
869
// This is used for indefinitely-long handlers like _changes that we don't want to track duration of
870
func (h *handler) logStatus(status int, message string) {
1✔
871
        h.setStatus(status, message)
1✔
872
        h.logDuration(false) // don't track actual time
1✔
873
}
1✔
874

875
func needRolesForAudit(db *db.DatabaseContext, domain base.UserIDDomain) bool {
1✔
876
        // early returns when auditing is disabled
1✔
877
        if db == nil ||
1✔
878
                db.Options.LoggingConfig == nil ||
1✔
879
                db.Options.LoggingConfig.Audit == nil ||
1✔
880
                !db.Options.LoggingConfig.Audit.Enabled {
2✔
881
                return false
1✔
882
        }
1✔
883

884
        // if we have any matching domains then we need the given roles
885
        // this loop should be ~free for len(0)
886
        for principal := range db.Options.LoggingConfig.Audit.DisabledRoles {
×
887
                if principal.Domain == string(domain) {
×
888
                        return true
×
889
                }
×
890
        }
891

892
        return false
×
893
}
894

895
// getSGUserRolesForAudit returns a list of role names for the given user, if audit role filtering is enabled.
896
func getSGUserRolesForAudit(db *db.DatabaseContext, user auth.User) []string {
1✔
897
        if user == nil {
2✔
898
                return nil
1✔
899
        }
1✔
900

901
        if !needRolesForAudit(db, base.UserDomainSyncGateway) {
2✔
902
                return nil
1✔
903
        }
1✔
904

905
        roles := user.GetRoles()
×
906
        if len(roles) == 0 {
×
907
                return nil
×
908
        }
×
909

910
        roleNames := make([]string, 0, len(roles))
×
911
        for _, role := range roles {
×
912
                roleNames = append(roleNames, role.Name())
×
913
        }
×
914

915
        return roleNames
×
916
}
917

918
// checkPublicAuth verifies that the current request is authenticated for the given database.
919
//
920
// NOTE: checkPublicAuth is not used for the admin interface.
921
func (h *handler) checkPublicAuth(dbCtx *db.DatabaseContext) (err error) {
1✔
922

1✔
923
        h.user = nil
1✔
924
        if dbCtx == nil {
2✔
925
                return nil
1✔
926
        }
1✔
927

928
        var auditFields base.AuditFields
1✔
929

1✔
930
        // Record Auth stats
1✔
931
        defer func(t time.Time) {
2✔
932
                delta := time.Since(t).Nanoseconds()
1✔
933
                dbCtx.DbStats.Security().TotalAuthTime.Add(delta)
1✔
934
                if err != nil {
2✔
935
                        dbCtx.DbStats.Security().AuthFailedCount.Add(1)
1✔
936
                        if errors.Is(err, ErrInvalidLogin) {
2✔
937
                                base.Audit(h.ctx(), base.AuditIDPublicUserAuthenticationFailed, auditFields)
1✔
938
                        }
1✔
939
                } else {
1✔
940
                        dbCtx.DbStats.Security().AuthSuccessCount.Add(1)
1✔
941

1✔
942
                        username := ""
1✔
943
                        if h.isGuest() {
2✔
944
                                username = base.GuestUsername
1✔
945
                        } else if h.user != nil {
3✔
946
                                username = h.user.Name()
1✔
947
                        }
1✔
948
                        roleNames := getSGUserRolesForAudit(dbCtx, h.user)
1✔
949
                        h.rqCtx = base.UserLogCtx(h.ctx(), username, base.UserDomainSyncGateway, roleNames)
1✔
950
                        base.Audit(h.ctx(), base.AuditIDPublicUserAuthenticated, auditFields)
1✔
951
                }
952
        }(time.Now())
953

954
        // If oidc enabled, check for bearer ID token
955
        if dbCtx.Options.OIDCOptions != nil || len(dbCtx.LocalJWTProviders) > 0 {
2✔
956
                if token := h.getBearerToken(); token != "" {
2✔
957
                        auditFields = base.AuditFields{base.AuditFieldAuthMethod: "bearer"}
1✔
958
                        var updates auth.PrincipalConfig
1✔
959
                        h.user, updates, err = dbCtx.Authenticator(h.ctx()).AuthenticateUntrustedJWT(token, dbCtx.OIDCProviders, dbCtx.LocalJWTProviders, h.getOIDCCallbackURL)
1✔
960
                        if h.user == nil || err != nil {
2✔
961
                                return ErrInvalidLogin
1✔
962
                        }
1✔
963
                        if issuer := h.user.JWTIssuer(); issuer != "" {
2✔
964
                                auditFields["oidc_issuer"] = issuer
1✔
965
                        }
1✔
966
                        if changes := checkJWTIssuerStillValid(h.ctx(), dbCtx, h.user); changes != nil {
2✔
967
                                updates = updates.Merge(*changes)
1✔
968
                        }
1✔
969
                        _, _, err := dbCtx.UpdatePrincipal(h.ctx(), &updates, true, true)
1✔
970
                        if err != nil {
1✔
971
                                return fmt.Errorf("failed to update OIDC user after sign-in: %w", err)
×
972
                        }
×
973
                        // TODO: could avoid this extra fetch if UpdatePrincipal returned the newly updated principal
974
                        if updates.Name != nil {
2✔
975
                                h.user, err = dbCtx.Authenticator(h.ctx()).GetUser(*updates.Name)
1✔
976
                        }
1✔
977
                        return err
1✔
978
                }
979

980
                /*
981
                * If unsupported/oidc testing is enabled
982
                * and this is a call on the token endpoint
983
                * and the username and password match those in the oidc default provider config
984
                * then authorize this request
985
                 */
986
                if strings.HasSuffix(h.rq.URL.Path, "/_oidc_testing/token") && dbCtx.Options.UnsupportedOptions != nil &&
1✔
987
                        dbCtx.Options.UnsupportedOptions.OidcTestProvider != nil && dbCtx.Options.UnsupportedOptions.OidcTestProvider.Enabled {
2✔
988
                        if username, password := h.getBasicAuth(); username != "" && password != "" {
2✔
989
                                provider := dbCtx.Options.OIDCOptions.Providers.GetProviderForIssuer(h.ctx(), issuerUrlForDB(h, dbCtx.Name), testProviderAudiences)
1✔
990
                                if provider != nil && provider.ValidationKey != nil {
2✔
991
                                        if base.ValDefault(provider.ClientID, "") == username && *provider.ValidationKey == password {
2✔
992
                                                auditFields = base.AuditFields{base.AuditFieldAuthMethod: "basic"}
1✔
993
                                                return nil
1✔
994
                                        }
1✔
995
                                }
996
                        }
997
                }
998
        }
999

1000
        // Check basic auth first
1001
        if !dbCtx.Options.DisablePasswordAuthentication {
2✔
1002
                if userName, password := h.getBasicAuth(); userName != "" {
2✔
1003
                        auditFields = base.AuditFields{base.AuditFieldAuthMethod: "basic"}
1✔
1004
                        h.user, err = dbCtx.Authenticator(h.ctx()).AuthenticateUser(userName, password)
1✔
1005
                        if err != nil {
1✔
1006
                                return err
×
1007
                        }
×
1008
                        if h.user == nil {
2✔
1009
                                auditFields["username"] = userName
1✔
1010
                                if dbCtx.Options.SendWWWAuthenticateHeader == nil || *dbCtx.Options.SendWWWAuthenticateHeader {
2✔
1011
                                        h.response.Header().Set("WWW-Authenticate", wwwAuthenticateHeader)
1✔
1012
                                }
1✔
1013
                                return ErrInvalidLogin
1✔
1014
                        }
1015
                        return nil
1✔
1016
                }
1017
        }
1018

1019
        // Check cookie
1020
        auditFields = base.AuditFields{base.AuditFieldAuthMethod: "cookie"}
1✔
1021
        h.user, err = dbCtx.Authenticator(h.ctx()).AuthenticateCookie(h.rq, h.response)
1✔
1022
        if err != nil && h.privs != publicPrivs {
2✔
1023
                return err
1✔
1024
        } else if h.user != nil {
3✔
1025
                return nil
1✔
1026
        }
1✔
1027

1028
        // No auth given -- check guest access
1029
        auditFields = base.AuditFields{base.AuditFieldAuthMethod: "guest"}
1✔
1030
        if h.user, err = dbCtx.Authenticator(h.ctx()).GetUser(""); err != nil {
1✔
1031
                return err
×
1032
        }
×
1033
        if h.privs == regularPrivs && h.user.Disabled() {
2✔
1034
                if dbCtx.Options.SendWWWAuthenticateHeader == nil || *dbCtx.Options.SendWWWAuthenticateHeader {
2✔
1035
                        h.response.Header().Set("WWW-Authenticate", wwwAuthenticateHeader)
1✔
1036
                }
1✔
1037
                if h.providedAuthCredentials() {
2✔
1038
                        return ErrInvalidLogin
1✔
1039
                }
1✔
1040
                return ErrLoginRequired
1✔
1041
        }
1042

1043
        return nil
1✔
1044
}
1045

1046
func checkJWTIssuerStillValid(ctx context.Context, dbCtx *db.DatabaseContext, user auth.User) *auth.PrincipalConfig {
1✔
1047
        issuer := user.JWTIssuer()
1✔
1048
        if issuer == "" {
2✔
1049
                return nil
1✔
1050
        }
1✔
1051
        providerStillValid := false
1✔
1052
        for _, provider := range dbCtx.OIDCProviders {
2✔
1053
                // No need to verify audiences, as that was done when the user was authenticated
1✔
1054
                if provider.Issuer == issuer {
2✔
1055
                        providerStillValid = true
1✔
1056
                        break
1✔
1057
                }
1058
        }
1059
        for _, provider := range dbCtx.LocalJWTProviders {
2✔
1060
                if provider.Issuer == issuer {
2✔
1061
                        providerStillValid = true
1✔
1062
                        break
1✔
1063
                }
1064
        }
1065
        if !providerStillValid {
2✔
1066
                base.InfofCtx(ctx, base.KeyAuth, "User %v uses OIDC issuer %v which is no longer configured. Revoking OIDC roles/channels.", base.UD(user.Name()), base.UD(issuer))
1✔
1067
                return &auth.PrincipalConfig{
1✔
1068
                        Name:        base.Ptr(user.Name()),
1✔
1069
                        JWTIssuer:   base.Ptr(""),
1✔
1070
                        JWTRoles:    base.Set{},
1✔
1071
                        JWTChannels: base.Set{},
1✔
1072
                }
1✔
1073
        }
1✔
1074
        return nil
1✔
1075
}
1076

1077
// checkAdminAuthenticationOnly simply checks whether a username / password combination is authenticated pulling the
1078
// credentials from the handler
1079
func (h *handler) checkAdminAuthenticationOnly() (bool, error) {
×
1080
        managementEndpoints, httpClient, err := h.server.ObtainManagementEndpointsAndHTTPClient()
×
1081
        if err != nil {
×
1082
                return false, base.HTTPErrorf(http.StatusInternalServerError, "Error getting management endpoints: %v", err)
×
1083
        }
×
1084

1085
        username, password := h.getBasicAuth()
×
1086
        if username == "" {
×
1087
                h.response.Header().Set("WWW-Authenticate", wwwAuthenticateHeader)
×
1088
                return false, ErrLoginRequired
×
1089
        }
×
1090

1091
        statusCode, _, err := doHTTPAuthRequest(h.ctx(), httpClient, username, password, "POST", "/pools/default/checkPermissions", managementEndpoints, nil)
×
1092
        if err != nil {
×
1093
                return false, base.HTTPErrorf(http.StatusInternalServerError, "Error performing HTTP auth request: %v", err)
×
1094
        }
×
1095

1096
        if statusCode == http.StatusUnauthorized {
×
1097
                base.Audit(h.ctx(), base.AuditIDAdminUserAuthenticationFailed, base.AuditFields{base.AuditFieldUserName: username})
×
1098
                return false, nil
×
1099
        }
×
1100

1101
        return true, nil
×
1102
}
1103

1104
func checkAdminAuth(ctx context.Context, bucketName, basicAuthUsername, basicAuthPassword string, attemptedHTTPOperation string, httpClient *http.Client, managementEndpoints []string, shouldCheckPermissions bool, accessPermissions []Permission, responsePermissions []Permission) (responsePermissionResults map[string]bool, statusCode int, err error) {
×
1105
        anyResponsePermFailed := false
×
1106
        permissionStatusCode, permResults, err := CheckPermissions(ctx, httpClient, managementEndpoints, bucketName, basicAuthUsername, basicAuthPassword, accessPermissions, responsePermissions)
×
1107
        if err != nil {
×
1108
                return nil, http.StatusInternalServerError, err
×
1109
        }
×
1110
        if len(responsePermissions) > 0 {
×
1111
                responsePermissionResults = permResults
×
1112
                for _, permResult := range permResults {
×
1113
                        if !permResult {
×
1114
                                anyResponsePermFailed = true
×
1115
                                break
×
1116
                        }
1117
                }
1118
        }
1119

1120
        // If the user has not logged in correctly we shouldn't continue to do any more work and return
1121
        if permissionStatusCode == http.StatusUnauthorized {
×
1122
                return nil, permissionStatusCode, nil
×
1123
        }
×
1124

1125
        if shouldCheckPermissions {
×
1126
                // If user has required accessPerms and all response perms return with statusOK
×
1127
                // Otherwise we need to fall through to continue as the user may have access to responsePermissions through roles.
×
1128
                if permissionStatusCode == http.StatusOK && !anyResponsePermFailed {
×
1129
                        return responsePermissionResults, http.StatusOK, nil
×
1130
                }
×
1131

1132
                // If status code was not 'ok' or 'forbidden' return
1133
                // If user has authenticated correctly but is not authorized with all permissions. We'll fall through to try
1134
                // with roles.
1135
                if permissionStatusCode != http.StatusOK && permissionStatusCode != http.StatusForbidden {
×
1136
                        return responsePermissionResults, permissionStatusCode, nil
×
1137
                }
×
1138
        }
1139

1140
        var requestRoles []RouteRole
×
1141
        if bucketName != "" {
×
1142
                requestRoles = BucketScopedEndpointRoles
×
1143
        } else {
×
1144
                if attemptedHTTPOperation == http.MethodGet || attemptedHTTPOperation == http.MethodHead || attemptedHTTPOperation == http.MethodOptions {
×
1145
                        requestRoles = ClusterScopedEndpointRolesRead
×
1146
                } else {
×
1147
                        requestRoles = ClusterScopedEndpointRolesWrite
×
1148
                }
×
1149
        }
1150

1151
        rolesStatusCode, err := CheckRoles(ctx, httpClient, managementEndpoints, basicAuthUsername, basicAuthPassword, requestRoles, bucketName)
×
1152
        if err != nil {
×
1153
                return nil, http.StatusInternalServerError, err
×
1154
        }
×
1155

1156
        // If a user has access through roles we're going to use this to mean they have access to all of the
1157
        // responsePermissions too so we'll iterate over these and set them to true.
1158
        if rolesStatusCode == http.StatusOK {
×
1159
                responsePermissionResults = make(map[string]bool)
×
1160
                for _, responsePerm := range responsePermissions {
×
1161
                        responsePermissionResults[responsePerm.PermissionName] = true
×
1162
                }
×
1163
                return responsePermissionResults, rolesStatusCode, nil
×
1164
        }
1165

1166
        // We want to select the most 'optimistic' status code here.
1167
        // If we got a status code 200 in the permissions check case but decided to check roles too (in the case where we
1168
        // didn't have access to all the responsePermissions) and ended up getting a 403 for the role check we want to
1169
        // return the 200 to allow the user handler access.
1170

1171
        // Start with role status code
1172
        resultStatusCode := rolesStatusCode
×
1173

×
1174
        // If resultStatus code is not okay (role check did 401, 403 or 500) and we're supposed to be allowing users in
×
1175
        // based on permissions we will select the code from the permission result as that may allow more access.
×
1176
        if resultStatusCode != http.StatusOK && shouldCheckPermissions {
×
1177
                resultStatusCode = permissionStatusCode
×
1178
        }
×
1179

1180
        return permResults, resultStatusCode, nil
×
1181
}
1182

1183
func (h *handler) assertAdminOnly() {
1✔
1184
        if h.privs != adminPrivs {
1✔
1185
                // TODO: CBG-1948
×
1186
                panic("Admin-only handler called without admin privileges, on " + h.rq.RequestURI)
×
1187
        }
1188
}
1189

1190
func (h *handler) PathVar(name string) string {
1✔
1191
        v := mux.Vars(h.rq)[name]
1✔
1192

1✔
1193
        // Escape special chars i.e. '+' otherwise they are removed by QueryUnescape()
1✔
1194
        v = strings.Replace(v, "+", "%2B", -1)
1✔
1195

1✔
1196
        // Before routing the URL we explicitly disabled expansion of %-escapes in the path
1✔
1197
        // (see function FixQuotedSlashes). So we have to unescape them now.
1✔
1198
        v, _ = url.QueryUnescape(v)
1✔
1199
        return v
1✔
1200
}
1✔
1201

1202
func (h *handler) SetPathVar(name string, value string) {
×
1203
        mux.Vars(h.rq)[name] = url.QueryEscape(value)
×
1204
}
×
1205

1206
func (h *handler) getQueryValues() url.Values {
1✔
1207
        if h.queryValues == nil {
2✔
1208
                h.queryValues = h.rq.URL.Query()
1✔
1209
        }
1✔
1210
        return h.queryValues
1✔
1211
}
1212

1213
func (h *handler) getQuery(query string) string {
1✔
1214
        return h.getQueryValues().Get(query)
1✔
1215
}
1✔
1216

1217
func (h *handler) getJSONStringQuery(query string) string {
1✔
1218
        return base.ConvertJSONString(h.getQuery(query))
1✔
1219
}
1✔
1220

1221
func (h *handler) getBoolQuery(query string) bool {
1✔
1222
        result, _ := h.getOptBoolQuery(query, false)
1✔
1223
        return result
1✔
1224
}
1✔
1225

1226
func (h *handler) getOptBoolQuery(query string, defaultValue bool) (result, isSet bool) {
1✔
1227
        q := h.getQuery(query)
1✔
1228
        if q == "" {
2✔
1229
                return defaultValue, false
1✔
1230
        }
1✔
1231
        return q == "true", true
1✔
1232
}
1233

1234
// Returns the integer value of a URL query, defaulting to 0 if unparseable
1235
func (h *handler) getIntQuery(query string, defaultValue uint64) (value uint64) {
1✔
1236
        return base.GetRestrictedIntQuery(h.getQueryValues(), query, defaultValue, 0, 0, false)
1✔
1237
}
1✔
1238

1239
func (h *handler) getJSONStringArrayQuery(param string) ([]string, error) {
1✔
1240
        var strings []string
1✔
1241
        value := h.getQuery(param)
1✔
1242
        if value != "" {
2✔
1243
                if err := base.JSONUnmarshal([]byte(value), &strings); err != nil {
1✔
1244
                        return nil, base.HTTPErrorf(http.StatusBadRequest, "%s URL param is not a JSON string array", param)
×
1245
                }
×
1246
        }
1247
        return strings, nil
1✔
1248
}
1249

1250
func (h *handler) userAgentIs(agent string) bool {
1✔
1251
        userAgent := h.rq.Header.Get(base.HTTPHeaderUserAgent)
1✔
1252
        return len(userAgent) > len(agent) && userAgent[len(agent)] == '/' && strings.HasPrefix(userAgent, agent)
1✔
1253
}
1✔
1254

1255
// Returns true if the header exists, and its value does NOT match the given etag.
1256
// (The etag parameter should not be double-quoted; the function will take care of that.)
1257
func (h *handler) headerDoesNotMatchEtag(etag string) bool {
1✔
1258
        value := h.rq.Header.Get("If-Match")
1✔
1259
        return value != "" && !strings.Contains(value, `"`+etag+`"`)
1✔
1260
}
1✔
1261

1262
func (h *handler) getEtag(headerName string) (string, error) {
1✔
1263
        value := h.rq.Header.Get(headerName)
1✔
1264
        if len(value) > 0 && !strings.Contains(value, `"`) {
1✔
1265
                return "", fmt.Errorf("ETag value is not quoted when trying to read the value")
×
1266
        }
×
1267
        eTag := strings.Trim(value, `"`)
1✔
1268
        return eTag, nil
1✔
1269
}
1270

1271
// Returns the request body as a raw byte array.
1272
func (h *handler) readBody() ([]byte, error) {
1✔
1273
        return io.ReadAll(h.requestBody)
1✔
1274
}
1✔
1275

1276
// Parses a JSON request body, returning it as a Body map.
1277
func (h *handler) readJSON() (db.Body, error) {
1✔
1278
        var body db.Body
1✔
1279
        return body, h.readJSONInto(&body)
1✔
1280
}
1✔
1281

1282
// Parses a JSON request body into a custom structure.
1283
func (h *handler) readJSONInto(into interface{}) error {
1✔
1284
        return ReadJSONFromMIME(h.rq.Header, h.requestBody, into)
1✔
1285
}
1✔
1286

1287
// readSanitizeJSONInto reads and sanitizes a JSON request body and returns DatabaseConfig.
1288
// Expands environment variables (if any) referenced in the config.
1289
func (h *handler) readSanitizeJSON(val interface{}) ([]byte, error) {
1✔
1290
        // Performs the Content-Type validation and Content-Encoding check.
1✔
1291
        input, err := processContentEncoding(h.rq.Header, h.requestBody, "application/json")
1✔
1292
        if err != nil {
1✔
1293
                return nil, err
×
1294
        }
×
1295

1296
        // Read body bytes to sanitize the content and substitute environment variables.
1297
        defer func() { _ = input.Close() }()
2✔
1298
        raw, err := io.ReadAll(input)
1✔
1299
        if err != nil {
1✔
1300
                return nil, err
×
1301
        }
×
1302

1303
        content, err := sanitiseConfig(h.ctx(), raw, h.server.Config.Unsupported.AllowDbConfigEnvVars)
1✔
1304
        if err != nil {
1✔
1305
                return nil, err
×
1306
        }
×
1307

1308
        // Decode the body bytes into target structure.
1309
        decoder := base.JSONDecoder(bytes.NewReader(content))
1✔
1310
        decoder.DisallowUnknownFields()
1✔
1311
        decoder.UseNumber()
1✔
1312
        err = decoder.Decode(&val)
1✔
1313

1✔
1314
        if err != nil {
2✔
1315
                err = base.WrapJSONUnknownFieldErr(err)
1✔
1316
                if errors.Cause(err) != base.ErrUnknownField {
2✔
1317
                        err = base.HTTPErrorf(http.StatusBadRequest, "Bad JSON: %s", err.Error())
1✔
1318
                }
1✔
1319
        }
1320
        return raw, err
1✔
1321
}
1322

1323
// readJavascript reads a javascript function from a request body.
1324
func (h *handler) readJavascript() (string, error) {
1✔
1325
        // Performs the Content-Type validation and Content-Encoding check.
1✔
1326
        input, err := processContentEncoding(h.rq.Header, h.requestBody, "application/javascript")
1✔
1327
        if err != nil {
1✔
1328
                return "", err
×
1329
        }
×
1330

1331
        defer func() { _ = input.Close() }()
2✔
1332
        jsBytes, err := io.ReadAll(input)
1✔
1333
        if err != nil {
1✔
1334
                return "", err
×
1335
        }
×
1336

1337
        return string(jsBytes), nil
1✔
1338
}
1339

1340
// readSanitizeJSONInto reads and sanitizes a JSON request body and returns raw bytes and DbConfig.
1341
// Expands environment variables (if any) referenced in the config.
1342
func (h *handler) readSanitizeDbConfigJSON() ([]byte, *DbConfig, error) {
1✔
1343
        var config DbConfig
1✔
1344
        rawBytes, err := h.readSanitizeJSON(&config)
1✔
1345
        if err != nil {
2✔
1346
                if errors.Cause(base.WrapJSONUnknownFieldErr(err)) == base.ErrUnknownField {
1✔
1347
                        err = base.HTTPErrorf(http.StatusBadRequest, "JSON Unknown Field: %s", err.Error())
×
1348
                }
×
1349
        }
1350
        return rawBytes, &config, err
1✔
1351
}
1352

1353
// Reads & parses the request body, handling either JSON or multipart.
1354
func (h *handler) readDocument() (db.Body, error) {
1✔
1355
        contentType, attrs, _ := mime.ParseMediaType(h.rq.Header.Get("Content-Type"))
1✔
1356
        switch contentType {
1✔
1357
        case "", "application/json":
1✔
1358
                return h.readJSON()
1✔
1359
        case "multipart/related":
1✔
1360
                if DebugMultipart {
1✔
1361
                        raw, err := h.readBody()
×
1362
                        if err != nil {
×
1363
                                return nil, err
×
1364
                        }
×
1365
                        reader := multipart.NewReader(bytes.NewReader(raw), attrs["boundary"])
×
1366
                        body, err := ReadMultipartDocument(reader)
×
1367
                        if err != nil {
×
1368
                                _ = os.WriteFile("GatewayPUT.mime", raw, 0600)
×
1369
                                base.WarnfCtx(h.ctx(), "Error reading MIME data: copied to file GatewayPUT.mime")
×
1370
                        }
×
1371
                        return body, err
×
1372
                } else {
1✔
1373
                        reader := multipart.NewReader(h.requestBody, attrs["boundary"])
1✔
1374
                        return ReadMultipartDocument(reader)
1✔
1375
                }
1✔
1376
        default:
×
1377
                return nil, base.HTTPErrorf(http.StatusUnsupportedMediaType, "Invalid content type %s", contentType)
×
1378
        }
1379
}
1380

1381
func (h *handler) requestAccepts(mimetype string) bool {
1✔
1382
        accept := h.rq.Header.Get("Accept")
1✔
1383
        return accept == "" || strings.Contains(accept, mimetype) || strings.Contains(accept, "*/*")
1✔
1384
}
1✔
1385

1386
func (h *handler) getBasicAuth() (username string, password string) {
1✔
1387
        auth := h.rq.Header.Get("Authorization")
1✔
1388
        if strings.HasPrefix(auth, "Basic ") {
2✔
1389
                decoded, err := base64.StdEncoding.DecodeString(auth[6:])
1✔
1390
                if err == nil {
2✔
1391
                        components := strings.SplitN(string(decoded), ":", 2)
1✔
1392
                        if len(components) == 2 {
2✔
1393
                                return components[0], components[1]
1✔
1394
                        }
1✔
1395
                }
1396
        }
1397
        return
1✔
1398
}
1399

1400
func (h *handler) getBearerToken() string {
1✔
1401
        auth := h.rq.Header.Get("Authorization")
1✔
1402
        if strings.HasPrefix(auth, "Bearer ") {
2✔
1403
                token := auth[7:]
1✔
1404
                return token
1✔
1405
        }
1✔
1406
        return ""
1✔
1407
}
1408

1409
// providedAuthCredentials returns true if basic auth or session auth is enabled
1410
func (h *handler) providedAuthCredentials() bool {
1✔
1411
        username, _ := h.getBasicAuth()
1✔
1412
        if username != "" {
2✔
1413
                return true
1✔
1414
        }
1✔
1415
        token := h.getBearerToken()
1✔
1416
        if token != "" {
1✔
1417
                return true
×
1418
        }
×
1419
        cookieName := auth.DefaultCookieName
1✔
1420
        if h.db != nil {
1✔
1421
                authenticator := h.db.Authenticator(h.ctx())
×
1422
                cookieName = authenticator.SessionCookieName
×
1423
        }
×
1424
        cookie, _ := h.rq.Cookie(cookieName)
1✔
1425
        return cookie != nil
1✔
1426
}
1427

1428
// taggedEffectiveUserName returns the tagged effective name of the user for the request.
1429
// e.g: '<ud>alice</ud>' or 'GUEST'
1430
func (h *handler) taggedEffectiveUserName() string {
1✔
1431
        if h.authorizedAdminUser != "" {
1✔
1432
                return base.UD(h.authorizedAdminUser).Redact() + " as ADMIN"
×
1433
        }
×
1434

1435
        if h.privs == adminPrivs || h.privs == metricsPrivs {
2✔
1436
                return "ADMIN"
1✔
1437
        }
1✔
1438

1439
        if h.user == nil {
2✔
1440
                return ""
1✔
1441
        }
1✔
1442

1443
        if name := h.user.Name(); name != "" {
2✔
1444
                return base.UD(name).Redact()
1✔
1445
        }
1✔
1446

1447
        return base.GuestUsername
1✔
1448
}
1449

1450
// formattedEffectiveUserName formats an effective name for appending to logs.
1451
// e.g: 'Did xyz (as %s)' or 'Did xyz (as <ud>alice</ud>)'
1452
func (h *handler) formattedEffectiveUserName() string {
1✔
1453
        if name := h.taggedEffectiveUserName(); name != "" {
2✔
1454
                return " (as " + name + ")"
1✔
1455
        }
1✔
1456

1457
        return ""
1✔
1458
}
1459

1460
// ////// RESPONSES:
1461

1462
func (h *handler) setHeader(name string, value string) {
1✔
1463
        h.response.Header().Set(name, value)
1✔
1464
}
1✔
1465

1466
// Adds an "Etag" header to the response, whose value is the parameter wrapped in double-quotes.
1467
func (h *handler) setEtag(etag string) {
1✔
1468
        h.setHeader("Etag", `"`+etag+`"`)
1✔
1469
        // (Note: etags should not contain double-quotes (per RFC7232 2.3) so no escaping needed)
1✔
1470
}
1✔
1471

1472
func (h *handler) setStatus(status int, message string) {
1✔
1473
        h.status = status
1✔
1474
        h.statusMessage = message
1✔
1475
}
1✔
1476

1477
func (h *handler) disableResponseCompression() {
1✔
1478
        switch r := h.response.(type) {
1✔
1479
        case *EncodedResponseWriter:
1✔
1480
                r.disableCompression()
1✔
1481
        }
1482
}
1483

1484
// Do not call from HTTP handlers. Use h.writeRawJSON/h.writeRawJSONStatus instead.
1485
// writeRawJSONWithoutClientVerification takes the given bytes and always writes the response as JSON,
1486
// without checking that the client can accept it.
1487
func (h *handler) writeRawJSONWithoutClientVerification(status int, b []byte) {
1✔
1488
        if h.rq.Method != "HEAD" {
2✔
1489
                h.setHeader("Content-Type", "application/json")
1✔
1490
                if len(b) < minCompressibleJSONSize {
2✔
1491
                        h.disableResponseCompression()
1✔
1492
                }
1✔
1493
                h.setHeader("Content-Length", fmt.Sprintf("%d", len(b)))
1✔
1494
                if status > 0 {
2✔
1495
                        h.response.WriteHeader(status)
1✔
1496
                        h.setStatus(status, "")
1✔
1497
                }
1✔
1498
                _, _ = h.response.Write(b)
1✔
1499
        } else if status > 0 {
2✔
1500
                h.response.WriteHeader(status)
1✔
1501
                h.setStatus(status, "")
1✔
1502
        }
1✔
1503
}
1504

1505
// writeJSON writes the given value as a JSON response with a 200 OK status.
1506
func (h *handler) writeJSON(value interface{}) {
1✔
1507
        h.writeJSONStatus(http.StatusOK, value)
1✔
1508
}
1✔
1509

1510
// Writes an object to the response in JSON format.
1511
// If status is nonzero, the header will be written with that status.
1512
func (h *handler) writeJSONStatus(status int, value interface{}) {
1✔
1513
        if !h.requestAccepts("application/json") {
1✔
1514
                base.WarnfCtx(h.ctx(), "Client won't accept JSON, only %s", h.rq.Header.Get("Accept"))
×
1515
                h.writeStatus(http.StatusNotAcceptable, "only application/json available")
×
1516
                return
×
1517
        }
×
1518

1519
        jsonOut, err := base.JSONMarshalCanonical(value)
1✔
1520
        if err != nil {
1✔
1521
                base.WarnfCtx(h.ctx(), "Couldn't serialize JSON for %v : %s", base.UD(value), err)
×
1522
                h.writeStatus(http.StatusInternalServerError, "JSON serialization failed")
×
1523
                return
×
1524
        }
×
1525
        if base.ValDefault(h.server.Config.API.Pretty, false) {
1✔
1526
                var buffer bytes.Buffer
×
1527
                _ = json.Indent(&buffer, jsonOut, "", "  ")
×
1528
                jsonOut = append(buffer.Bytes(), '\n')
×
1529
        }
×
1530

1531
        h.writeRawJSONWithoutClientVerification(status, jsonOut)
1✔
1532
}
1533

1534
// writeRawJSON writes the given bytes as a JSON response with a 200 OK status.
1535
func (h *handler) writeRawJSON(b []byte) {
1✔
1536
        h.writeRawJSONStatus(http.StatusOK, b)
1✔
1537
}
1✔
1538

1539
// writeRawJSONStatus writes the given bytes as a JSON response.
1540
// If status is nonzero, the header will be written with that status.
1541
func (h *handler) writeRawJSONStatus(status int, b []byte) {
1✔
1542
        if !h.requestAccepts("application/json") {
1✔
1543
                base.WarnfCtx(h.ctx(), "Client won't accept JSON, only %s", h.rq.Header.Get("Accept"))
×
1544
                h.writeStatus(http.StatusNotAcceptable, "only application/json available")
×
1545
                return
×
1546
        }
×
1547

1548
        h.writeRawJSONWithoutClientVerification(status, b)
1✔
1549
}
1550

1551
// writeRawJSON writes the given bytes as a plaintext response with a 200 OK status.
1552
func (h *handler) writeText(value []byte) {
1✔
1553
        h.writeTextStatus(http.StatusOK, value)
1✔
1554
}
1✔
1555

1556
// writeTextStatus writes the given bytes as a plaintext response.
1557
// If status is nonzero, the header will be written with that status.
1558
func (h *handler) writeTextStatus(status int, value []byte) {
1✔
1559
        h.writeWithMimetypeStatus(status, value, "text/plain")
1✔
1560
}
1✔
1561

1562
func (h *handler) writeJavascript(js string) {
1✔
1563
        h.writeWithMimetypeStatus(http.StatusOK, []byte(js), "application/javascript")
1✔
1564
}
1✔
1565

1566
// writeTextStatus writes the given bytes as a plaintext response.
1567
// If status is nonzero, the header will be written with that status.
1568
func (h *handler) writeWithMimetypeStatus(status int, value []byte, mimetype string) {
1✔
1569
        if !h.requestAccepts(mimetype) {
1✔
1570
                base.WarnfCtx(h.ctx(), "Client won't accept %s, only %s", mimetype, h.rq.Header.Get("Accept"))
×
1571
                h.writeStatus(http.StatusNotAcceptable, fmt.Sprintf("only %s available", mimetype))
×
1572
                return
×
1573
        }
×
1574

1575
        h.setHeader("Content-Type", mimetype+"; charset=UTF-8")
1✔
1576
        h.setHeader("Content-Length", fmt.Sprintf("%d", len(value)))
1✔
1577
        if status > 0 {
2✔
1578
                h.response.WriteHeader(status)
1✔
1579
                h.setStatus(status, http.StatusText(status))
1✔
1580
        }
1✔
1581
        // RFC-9110: "HEAD method .. the server MUST NOT send content in the response"
1582
        if h.rq.Method != http.MethodHead {
2✔
1583
                _, _ = h.response.Write(value)
1✔
1584
        }
1✔
1585
}
1586

1587
func (h *handler) addJSON(value interface{}) error {
1✔
1588
        encoder := base.JSONEncoderCanonical(h.response)
1✔
1589
        err := encoder.Encode(value)
1✔
1590
        if err != nil {
1✔
1591
                brokenPipeError := strings.Contains(err.Error(), "write: broken pipe")
×
1592
                connectionResetError := strings.Contains(err.Error(), "write: connection reset")
×
1593
                if brokenPipeError || connectionResetError {
×
1594
                        base.DebugfCtx(h.ctx(), base.KeyCRUD, "Couldn't serialize document body, HTTP client closed connection")
×
1595
                        return err
×
1596
                } else {
×
1597
                        base.WarnfCtx(h.ctx(), "Couldn't serialize JSON for %s", err)
×
1598
                        h.writeStatus(http.StatusInternalServerError, "Couldn't serialize document body")
×
1599
                }
×
1600
        }
1601
        return nil
1✔
1602
}
1603

1604
func (h *handler) writeMultipart(subtype string, callback func(*multipart.Writer) error) error {
1✔
1605
        if !h.requestAccepts("multipart/") {
1✔
1606
                return base.HTTPErrorf(http.StatusNotAcceptable, "Response is multipart")
×
1607
        }
×
1608

1609
        // Get the output stream. Due to a CouchDB bug, if we're sending to it we need to buffer the
1610
        // output in memory so we can trim the final bytes.
1611
        var output io.Writer
1✔
1612
        var buffer bytes.Buffer
1✔
1613
        if h.userAgentIs("CouchDB") {
1✔
1614
                output = &buffer
×
1615
        } else {
1✔
1616
                output = h.response
1✔
1617
        }
1✔
1618

1619
        writer := multipart.NewWriter(output)
1✔
1620
        h.setHeader("Content-Type",
1✔
1621
                fmt.Sprintf("multipart/%s; boundary=%q", subtype, writer.Boundary()))
1✔
1622

1✔
1623
        err := callback(writer)
1✔
1624
        _ = writer.Close()
1✔
1625

1✔
1626
        if err == nil && output == &buffer {
1✔
1627
                // Trim trailing newline; CouchDB is allergic to it:
×
1628
                _, err = h.response.Write(bytes.TrimRight(buffer.Bytes(), "\r\n"))
×
1629
        }
×
1630
        return err
1✔
1631
}
1632

1633
func (h *handler) flush() {
1✔
1634
        h.response.(http.Flusher).Flush()
1✔
1635
}
1✔
1636

1637
// If the error parameter is non-nil, sets the response status code appropriately and
1638
// writes a CouchDB-style JSON description to the body.
1639
func (h *handler) writeError(err error) {
1✔
1640
        if err != nil {
2✔
1641
                status, message := base.ErrorAsHTTPStatus(err)
1✔
1642
                h.writeStatus(status, message)
1✔
1643
                if status >= 500 {
2✔
1644
                        if status == http.StatusServiceUnavailable {
2✔
1645
                                base.InfofCtx(h.ctx(), base.KeyHTTP, "%s: %v", h.formatSerialNumber(), err)
1✔
1646
                        } else {
2✔
1647
                                base.ErrorfCtx(h.ctx(), "%s: %v", h.formatSerialNumber(), err)
1✔
1648
                        }
1✔
1649
                }
1650
        }
1651
}
1652

1653
// Writes the response status code, and if it's an error writes a JSON description to the body.
1654
func (h *handler) writeStatus(status int, message string) {
1✔
1655
        if status < 300 {
2✔
1656
                h.response.WriteHeader(status)
1✔
1657
                h.setStatus(status, message)
1✔
1658
                return
1✔
1659
        }
1✔
1660
        // Got an error:
1661
        var errorStr string
1✔
1662
        switch status {
1✔
1663
        case http.StatusNotFound:
1✔
1664
                errorStr = "not_found"
1✔
1665
        case http.StatusConflict:
1✔
1666
                errorStr = "conflict"
1✔
1667
        default:
1✔
1668
                errorStr = http.StatusText(status)
1✔
1669
                if errorStr == "" {
1✔
1670
                        errorStr = fmt.Sprintf("%d", status)
×
1671
                }
×
1672
        }
1673

1674
        h.disableResponseCompression()
1✔
1675
        h.setHeader("Content-Type", "application/json")
1✔
1676
        h.response.WriteHeader(status)
1✔
1677
        h.setStatus(status, message)
1✔
1678

1✔
1679
        _, _ = h.response.Write([]byte(`{"error":"` + errorStr + `","reason":` + base.ConvertToJSONString(message) + `}`))
1✔
1680
}
1681

1682
var kRangeRegex = regexp.MustCompile("^bytes=(\\d+)?-(\\d+)?$")
1683

1684
// Detects and partially HTTP content range requests.
1685
// If the request _can_ accept ranges, sets the "Accept-Ranges" response header to "bytes".
1686
//
1687
// If there is no Range: request header, or if its valid is invalid, returns a status of 200,
1688
// meaning that the caller should return the entire response as usual.
1689
//
1690
// If there is a request range but it exceeds the contentLength, returns status 416. The caller
1691
// should treat this as an error and abort, returning that HTTP status code.
1692
//
1693
// If there is a useable range, it returns status 206 and the start and end in Go slice terms, i.e.
1694
// starting at 0 and with the end non-inclusive. It also adds a "Content-Range" response header.
1695
// It is then the _caller's_ responsibility to set it as the response status code (by calling
1696
// h.response.WriteHeader(status)), and then write the indicated subrange of the response data.
1697
func (h *handler) handleRange(contentLength uint64) (status int, start uint64, end uint64) {
1✔
1698
        status = http.StatusOK
1✔
1699
        if h.rq.Method == "GET" || h.rq.Method == "HEAD" {
2✔
1700
                h.setHeader("Accept-Ranges", "bytes")
1✔
1701
                status, start, end = parseHTTPRangeHeader(h.rq.Header.Get("Range"), contentLength)
1✔
1702
                if status == http.StatusPartialContent {
2✔
1703
                        h.setHeader("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, contentLength))
1✔
1704
                        h.setStatus(http.StatusPartialContent, "Partial Content")
1✔
1705
                        end += 1 // make end non-inclusive, as in Go slices
1✔
1706
                }
1✔
1707
        }
1708
        return
1✔
1709
}
1710

1711
// Given an HTTP "Range:" header value, parses it and returns the approppriate HTTP status code,
1712
// and the numeric byte range if appropriate:
1713
//   - If the Range header is empty or syntactically invalid, it ignores it and returns status=200.
1714
//   - If the header is valid but exceeds the contentLength, it returns status=416 (Not Satisfiable).
1715
//   - Otherwise it returns status=206 and sets the start and end values in HTTP terms, i.e. with
1716
//     the first byte numbered 0, and the end value inclusive (so the first 100 bytes are 0-99.)
1717
func parseHTTPRangeHeader(rangeStr string, contentLength uint64) (status int, start uint64, end uint64) {
1✔
1718
        // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
1✔
1719
        status = http.StatusOK
1✔
1720
        if rangeStr == "" {
2✔
1721
                return
1✔
1722
        }
1✔
1723
        match := kRangeRegex.FindStringSubmatch(rangeStr)
1✔
1724
        if match == nil || (match[1] == "" && match[2] == "") {
2✔
1725
                return
1✔
1726
        }
1✔
1727
        startStr, endStr := match[1], match[2]
1✔
1728
        var err error
1✔
1729

1✔
1730
        start = 0
1✔
1731
        if startStr != "" {
2✔
1732
                // byte-range-spec
1✔
1733
                if start, err = strconv.ParseUint(startStr, 10, 64); err != nil {
2✔
1734
                        start = math.MaxUint64 // string is all digits, so must just be too big for uint64
1✔
1735
                }
1✔
1736
        } else if endStr == "" {
1✔
1737
                return // "-" is an invalid range spec
×
1738
        }
×
1739

1740
        end = contentLength - 1
1✔
1741
        if endStr != "" {
2✔
1742
                if end, err = strconv.ParseUint(endStr, 10, 64); err != nil {
2✔
1743
                        end = math.MaxUint64 // string is all digits, so must just be too big for uint64
1✔
1744
                }
1✔
1745
                if startStr == "" {
2✔
1746
                        // suffix-range-spec ("-nnn" means the last nnn bytes)
1✔
1747
                        if end == 0 {
2✔
1748
                                return http.StatusRequestedRangeNotSatisfiable, 0, 0
1✔
1749
                        } else if contentLength == 0 {
3✔
1750
                                return
1✔
1751
                        } else if end > contentLength {
3✔
1752
                                end = contentLength
1✔
1753
                        }
1✔
1754
                        start = contentLength - end
1✔
1755
                        end = contentLength - 1
1✔
1756
                } else {
1✔
1757
                        if end < start {
2✔
1758
                                return // invalid range
1✔
1759
                        }
1✔
1760
                        if end >= contentLength {
2✔
1761
                                end = contentLength - 1 // trim to end of content
1✔
1762
                        }
1✔
1763
                }
1764
        }
1765
        if start >= contentLength {
2✔
1766
                return http.StatusRequestedRangeNotSatisfiable, 0, 0
1✔
1767
        } else if start == 0 && end == contentLength-1 {
3✔
1768
                return // no-op
1✔
1769
        }
1✔
1770

1771
        // OK, it's a subrange:
1772
        status = http.StatusPartialContent
1✔
1773
        return
1✔
1774
}
1775

1776
// formatSerialNumber returns the formatted serial number
1777
func (h *handler) formatSerialNumber() string {
1✔
1778
        if h.formattedSerialNumber == "" {
2✔
1779
                h.formattedSerialNumber = fmt.Sprintf("#%03d", h.serialNumber)
1✔
1780
        }
1✔
1781
        return h.formattedSerialNumber
1✔
1782
}
1783

1784
// shouldShowProductVersion returns whether the handler should show detailed product info (version).
1785
// Admin requests can always see this, regardless of the HideProductVersion setting.
1786
func (h *handler) shouldShowProductVersion() bool {
1✔
1787
        hideProductVersion := base.ValDefault(h.server.Config.API.HideProductVersion, false)
1✔
1788
        return h.serverType == adminServer || !hideProductVersion
1✔
1789
}
1✔
1790

1791
func requiresWritePermission(accessPermissions []Permission) bool {
1✔
1792
        for _, permission := range accessPermissions {
2✔
1793
                if permission == PermWriteAppData {
2✔
1794
                        return true
1✔
1795
                }
1✔
1796
        }
1797
        return false
1✔
1798
}
1799

1800
func (h *handler) isBlipSync() bool {
1✔
1801
        return h.pathTemplateContains("_blipsync")
1✔
1802
}
1✔
1803

1804
// Checks whether the mux path template for the current route contains the specified pattern
1805
func (h *handler) pathTemplateContains(pattern string) bool {
1✔
1806
        route := mux.CurrentRoute(h.rq)
1✔
1807
        pathTemplate, err := route.GetPathTemplate()
1✔
1808
        if err != nil {
1✔
1809
                return false
×
1810
        }
×
1811
        return strings.Contains(pathTemplate, pattern)
1✔
1812
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc