* chore(deps): bump the go group across 7 directories with 5 updates Bumps the go group with 4 updates in the / directory: [github.com/coder/websocket](https://github.com/coder/websocket), [github.com/docker/docker](https://github.com/docker/docker), [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) and [mvdan.cc/gofumpt](https://github.com/mvdan/gofumpt). Bumps the go group with 2 updates in the /api directory: [github.com/coder/websocket](https://github.com/coder/websocket) and [github.com/hashicorp/boundary/sdk](https://github.com/hashicorp/boundary). Bumps the go group with 1 update in the /plugins/boundary/mains/aws directory: [github.com/hashicorp/boundary/sdk](https://github.com/hashicorp/boundary). Bumps the go group with 1 update in the /plugins/boundary/mains/azure directory: [github.com/hashicorp/boundary/sdk](https://github.com/hashicorp/boundary). Bumps the go group with 1 update in the /plugins/boundary/mains/gcp directory: [github.com/hashicorp/boundary/sdk](https://github.com/hashicorp/boundary). Bumps the go group with 1 update in the /plugins/boundary/mains/minio directory: [github.com/hashicorp/boundary/sdk](https://github.com/hashicorp/boundary). Bumps the go group with 1 update in the /sdk directory: [github.com/coder/websocket](https://github.com/coder/websocket). Updates `github.com/coder/websocket` from 1.8.13 to 1.8.14 - [Release notes](https://github.com/coder/websocket/releases) - [Commits](https://github.com/coder/websocket/compare/v1.8.13...v1.8.14) Updates `github.com/docker/docker` from 28.3.3+incompatible to 28.4.0+incompatible - [Release notes](https://github.com/docker/docker/releases) - [Commits](https://github.com/docker/docker/compare/v28.3.3...v28.4.0) Updates `github.com/prometheus/client_golang` from 1.23.0 to 1.23.2 - [Release notes](https://github.com/prometheus/client_golang/releases) - [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md) - [Commits](https://github.com/prometheus/client_golang/compare/v1.23.0...v1.23.2) Updates `mvdan.cc/gofumpt` from 0.8.0 to 0.9.0 - [Release notes](https://github.com/mvdan/gofumpt/releases) - [Changelog](https://github.com/mvdan/gofumpt/blob/master/CHANGELOG.md) - [Commits](https://github.com/mvdan/gofumpt/compare/v0.8.0...v0.9.0) Updates `github.com/coder/websocket` from 1.8.13 to 1.8.14 - [Release notes](https://github.com/coder/websocket/releases) - [Commits](https://github.com/coder/websocket/compare/v1.8.13...v1.8.14) Updates `github.com/hashicorp/boundary/sdk` from 0.0.54 to 0.0.55 - [Release notes](https://github.com/hashicorp/boundary/releases) - [Changelog](https://github.com/hashicorp/boundary/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/boundary/compare/api/v0.0.54...api/v0.0.55) Updates `github.com/hashicorp/boundary/sdk` from 0.0.54 to 0.0.55 - [Release notes](https://github.com/hashicorp/boundary/releases) - [Changelog](https://github.com/hashicorp/boundary/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/boundary/compare/api/v0.0.54...api/v0.0.55) Updates `github.com/hashicorp/boundary/sdk` from 0.0.54 to 0.0.55 - [Release notes](https://github.com/hashicorp/boundary/releases) - [Changelog](https://github.com/hashicorp/boundary/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/boundary/compare/api/v0.0.54...api/v0.0.55) Updates `github.com/hashicorp/boundary/sdk` from 0.0.54 to 0.0.55 - [Release notes](https://github.com/hashicorp/boundary/releases) - [Changelog](https://github.com/hashicorp/boundary/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/boundary/compare/api/v0.0.54...api/v0.0.55) Updates `github.com/hashicorp/boundary/sdk` from 0.0.54 to 0.0.55 - [Release notes](https://github.com/hashicorp/boundary/releases) - [Changelog](https://github.com/hashicorp/boundary/blob/main/CHANGELOG.md) - [Commits](https://github.com/hashicorp/boundary/compare/api/v0.0.54...api/v0.0.55) Updates `github.com/coder/websocket` from 1.8.13 to 1.8.14 - [Release notes](https://github.com/coder/websocket/releases) - [Commits](https://github.com/coder/websocket/compare/v1.8.13...v1.8.14) --- updated-dependencies: - dependency-name: github.com/coder/websocket dependency-version: 1.8.14 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go - dependency-name: github.com/docker/docker dependency-version: 28.4.0+incompatible dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go - dependency-name: github.com/prometheus/client_golang dependency-version: 1.23.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go - dependency-name: mvdan.cc/gofumpt dependency-version: 0.9.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go - dependency-name: github.com/coder/websocket dependency-version: 1.8.14 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go - dependency-name: github.com/hashicorp/boundary/sdk dependency-version: 0.0.55 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go - dependency-name: github.com/hashicorp/boundary/sdk dependency-version: 0.0.55 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go - dependency-name: github.com/hashicorp/boundary/sdk dependency-version: 0.0.55 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go - dependency-name: github.com/hashicorp/boundary/sdk dependency-version: 0.0.55 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go - dependency-name: github.com/hashicorp/boundary/sdk dependency-version: 0.0.55 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go - dependency-name: github.com/coder/websocket dependency-version: 1.8.14 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go ... Signed-off-by: dependabot[bot] <support@github.com> * Format files with latest gofumpt --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Johan Brandhorst-Satzkorn <johan.brandhorst@gmail.com>
182 lines
4.9 KiB
Go
182 lines
4.9 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package ratelimit
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math"
|
|
"net/http"
|
|
"regexp"
|
|
|
|
"github.com/hashicorp/boundary/globals"
|
|
"github.com/hashicorp/boundary/internal/event"
|
|
"github.com/hashicorp/boundary/internal/types/action"
|
|
"github.com/hashicorp/boundary/internal/types/resource"
|
|
"github.com/hashicorp/go-rate"
|
|
)
|
|
|
|
var (
|
|
errUnknownResource = &extractResourceActionErr{http.StatusNotFound, "unknown resource"}
|
|
errUnknownAction = &extractResourceActionErr{http.StatusBadRequest, "unknown action"}
|
|
errUnsupportedAction = &extractResourceActionErr{http.StatusMethodNotAllowed, "invalid action"}
|
|
)
|
|
|
|
type extractResourceActionErr struct {
|
|
statusCode int
|
|
msg string
|
|
}
|
|
|
|
func (e *extractResourceActionErr) Error() string {
|
|
return e.msg
|
|
}
|
|
|
|
var pathRegex = regexp.MustCompile(`/v1/(?P<resource>[\w-]+)((/(?P<id>[^:]+))?(:(?P<action>[\w-:]+)?)?)?`)
|
|
|
|
func extractResourceAction(path, method string) (res, act string, err error) {
|
|
var id string
|
|
|
|
var r resource.Type
|
|
var ok bool
|
|
var actionSet action.ActionSet
|
|
|
|
// TODO: replace regex with lexer
|
|
match := pathRegex.FindStringSubmatch(path)
|
|
for i, name := range pathRegex.SubexpNames() {
|
|
switch name {
|
|
case "resource":
|
|
res = match[i]
|
|
r, ok = resource.FromPlural(res)
|
|
if !ok {
|
|
res = resource.Unknown.String()
|
|
err = errUnknownResource
|
|
return res, act, err
|
|
}
|
|
res = r.String()
|
|
case "action":
|
|
act = match[i]
|
|
if act != "" {
|
|
actionSet, err = action.ActionSetForResource(r)
|
|
if err != nil {
|
|
act = action.Unknown.String()
|
|
err = errUnknownAction
|
|
return res, act, err
|
|
}
|
|
at, ok := action.Map[act]
|
|
if !ok {
|
|
act = action.Unknown.String()
|
|
err = errUnknownAction
|
|
return res, act, err
|
|
}
|
|
if !actionSet.HasAction(at) {
|
|
err = errUnsupportedAction
|
|
return res, act, err
|
|
}
|
|
}
|
|
case "id":
|
|
id = match[i]
|
|
}
|
|
}
|
|
|
|
switch act {
|
|
case "":
|
|
switch id {
|
|
case "":
|
|
switch method {
|
|
case http.MethodGet:
|
|
act = action.List.String()
|
|
case http.MethodPost:
|
|
act = action.Create.String()
|
|
default:
|
|
act = action.Unknown.String()
|
|
err = errUnsupportedAction
|
|
return res, act, err
|
|
}
|
|
default:
|
|
switch method {
|
|
case http.MethodGet:
|
|
act = action.Read.String()
|
|
case http.MethodDelete:
|
|
act = action.Delete.String()
|
|
case http.MethodPatch:
|
|
act = action.Update.String()
|
|
default:
|
|
act = action.Unknown.String()
|
|
err = errUnsupportedAction
|
|
return res, act, err
|
|
}
|
|
}
|
|
}
|
|
return res, act, err
|
|
}
|
|
|
|
// LimiterFunc returns a rate.Limiter
|
|
type LimiterFunc func() Limiter
|
|
|
|
// Handler is an http middleware handler that checks if a request is allowed
|
|
// using the rate limiter returned by f. If the request is allowed, the next handler
|
|
// is called. Otherwise a 429 is returned with the Retry-After response header
|
|
// set to the number of seconds the client should wait to make it's next request.
|
|
func Handler(ctx context.Context, f LimiterFunc, next http.Handler) http.Handler {
|
|
const op = "ratelimit.Handler"
|
|
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
l := f()
|
|
|
|
reqInfo, ok := event.RequestInfoFromContext(req.Context())
|
|
if !ok || reqInfo == nil || reqInfo.ClientIp == "" {
|
|
rw.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
authtoken, ok := req.Context().Value(globals.ContextAuthTokenPublicIdKey).(string)
|
|
if !ok {
|
|
rw.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
res, a, err := extractResourceAction(req.URL.Path, req.Method)
|
|
if err != nil {
|
|
if extractErr, ok := err.(*extractResourceActionErr); ok {
|
|
rw.WriteHeader(extractErr.statusCode)
|
|
return
|
|
}
|
|
rw.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
allowed, quota, err := l.Allow(res, a, reqInfo.ClientIp, authtoken)
|
|
if err != nil {
|
|
if errFull, ok := err.(*rate.ErrLimiterFull); ok {
|
|
rw.Header().Add("Retry-After", fmt.Sprintf("%.0f", math.Ceil(errFull.RetryIn.Seconds())))
|
|
rw.WriteHeader(http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
// The only other error here should be rate.ErrLimitNotFound, which
|
|
// shouldn't be possible given how we initialize the limiter and
|
|
// the checks done by extractResourceAction.
|
|
rw.WriteHeader(http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
l.SetUsageHeader(quota, rw.Header())
|
|
if err := l.SetPolicyHeader(res, a, rw.Header()); err != nil {
|
|
// Wrap error to emit an error event. An error here would be
|
|
// unexpected, since the only possible error would be
|
|
// ErrLimitPolicyNotFound which would have been returned by Allow
|
|
// and handled above. And even that would be unexpected, given how
|
|
// the limiter is initialized and the checks done by
|
|
// extractResourceAction.
|
|
event.WriteError(ctx, op, fmt.Errorf("failed to set policy header: %w", err))
|
|
}
|
|
|
|
if !allowed {
|
|
rw.Header().Add("Retry-After", fmt.Sprintf("%.0f", math.Ceil(quota.ResetsIn().Seconds())))
|
|
rw.WriteHeader(http.StatusTooManyRequests)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(rw, req)
|
|
})
|
|
}
|