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

mendersoftware / mender-server / 2020499861

03 Sep 2025 08:29PM UTC coverage: 65.655% (+0.3%) from 65.357%
2020499861

Pull #915

gitlab-ci

mzedel
fix(gui): let theme switch also be reflected via external style definitions

Signed-off-by: Manuel Zedel <manuel.zedel@northern.tech>
Pull Request #915: fix(gui): let theme switch also be reflected via external style definitions

29313 of 44647 relevant lines covered (65.66%)

1.44 hits per line

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

0.0
/backend/pkg/rate/limit_http.go
1
package rate
2

3
import (
4
        "bytes"
5
        "context"
6
        "encoding/json"
7
        "errors"
8
        "fmt"
9
        "math"
10
        "net/http"
11
        "strconv"
12
        "text/template"
13
        "time"
14

15
        "github.com/gin-gonic/gin"
16

17
        "github.com/mendersoftware/mender-server/pkg/identity"
18
        "github.com/mendersoftware/mender-server/pkg/requestid"
19
        "github.com/mendersoftware/mender-server/pkg/rest.utils"
20
)
21

22
// TooManyRequestsError is the error type returned when hitting the rate
23
// limits.
24
type TooManyRequestsError struct {
25
        Delay time.Duration
26
}
27

28
func (err *TooManyRequestsError) Error() string {
×
29
        return "too many requests"
×
30
}
×
31

32
type templateEventLimiter struct {
33
        limiter       EventLimiter
34
        eventTemplate *template.Template
35
}
36

37
// HTTPLimiter combines a set of EventLimiter (groups) and a http.ServeMux
38
// for routing API endpoints to a EventLimiter.
39
//
40
// Every EventLimiter comes with an eventTemplate for grouping events by
41
// a Go template expression. The data to the Go template can be customized by
42
// calling WithTemplateDataFunc and WithTemplateFuncs.
43
//
44
// Additional routes are added by calling MatchHTTPPattern which accepts a HTTP
45
// pattern and a target group which also accepts Go template expressions.
46
//
47
// The HTTPLimiter implements both the standard http.Handler and
48
// gin.HandlerFunc (see (*HTTPLimiter).MiddlewareGin) for use with either the
49
// standard library or as a middleware for the gin-gonic framework.
50
type HTTPLimiter struct {
51
        // rootTemplate stores the root Template that is inherited by all
52
        // template string parameters (event and group strings).
53
        rootTemplate *template.Template
54
        // templateDataCallback is a callback that is called before executing
55
        // template strings and should output the template data used.
56
        //
57
        // See defaultTemplateData.
58
        templateDataCallback func(r *http.Request) any
59

60
        // httpMux implements the request router to a ratelimiter match instance
61
        httpMux *http.ServeMux
62

63
        // limiterGroups maps the group name to rate limiters.
64
        limiterGroups map[string]*templateEventLimiter
65

66
        // rewriteRequests decides whether to autmatically rewrite the request
67
        // URL using X-Forwarded-* headers.
68
        rewriteRequests bool
69

70
        // fallbackReservation is the reservation returned when no rules
71
        // matches. It is either returning a constant OK Reservation (default)
72
        // or Reject all requests if WithRejectUnmatched is set.
73
        fallbackReservation Reservation
74
}
75

76
// defaultTemplateData is the default callback for executing template strings.
77
func defaultTemplateData(r *http.Request) any {
×
78
        id := identity.FromContext(r.Context())
×
79
        ctx := map[string]any{
×
80
                "Identity": id,
×
81
                // "Request":  r, // not needed, but could be handy
×
82
        }
×
83
        return ctx
×
84
}
×
85

86
// NewHTTPLimiter initializes an empty HTTPLimiter.
87
// The limiter is built by calling AddRateLimitGroup and AddMatchExpression.
88
func NewHTTPLimiter() *HTTPLimiter {
×
89
        return &HTTPLimiter{
×
90
                rootTemplate:         template.New("").Option("missingkey=zero"),
×
91
                httpMux:              http.NewServeMux(),
×
92
                templateDataCallback: defaultTemplateData,
×
93
                limiterGroups:        make(map[string]*templateEventLimiter),
×
94
                fallbackReservation:  okReservation{},
×
95
        }
×
96
}
×
97

98
func (h *HTTPLimiter) WithTemplateDataFunc(f func(*http.Request) any) *HTTPLimiter {
×
99
        h.templateDataCallback = f
×
100
        return h
×
101
}
×
102

103
func (h *HTTPLimiter) WithTemplateFuncs(funcs map[string]any) *HTTPLimiter {
×
104
        h.rootTemplate.Funcs(funcs)
×
105
        return h
×
106
}
×
107

108
func (h *HTTPLimiter) WithRewriteRequests(rewrite bool) *HTTPLimiter {
×
109
        h.rewriteRequests = rewrite
×
110
        return h
×
111
}
×
112

113
func (h *HTTPLimiter) WithRejectUnmatched() *HTTPLimiter {
×
114
        h.fallbackReservation = rejectReservation{}
×
115
        return h
×
116
}
×
117

118
func (h *HTTPLimiter) AddRateLimitGroup(limiter EventLimiter, group, eventTemplate string) error {
×
119
        t, err := h.rootTemplate.Clone()
×
120
        if err == nil {
×
121
                _, err = t.Parse(eventTemplate)
×
122
        }
×
123
        if err != nil {
×
124
                return fmt.Errorf("failed to compile event template: %w", err)
×
125
        }
×
126
        h.limiterGroups[group] = &templateEventLimiter{
×
127
                limiter:       limiter,
×
128
                eventTemplate: t,
×
129
        }
×
130
        return nil
×
131
}
132

133
// AddMatchExpression creates a new route using pattern to apply the
134
// rate limiter that is matched by groupTemplate Go Template.
135
func (h *HTTPLimiter) AddMatchExpression(
136
        pattern, groupTemplate string,
137
) error {
×
138
        var (
×
139
                t   *template.Template
×
140
                err error
×
141
        )
×
142
        // Compile eventTemplate:
×
143
        t, err = h.rootTemplate.Clone()
×
144
        if err == nil {
×
145
                _, err = t.Parse(groupTemplate)
×
146
        }
×
147
        if err != nil {
×
148
                return fmt.Errorf("error parsing group_template: %w", err)
×
149
        }
×
150
        limiterMatcher := matcher{
×
151
                HTTPLimiter:   h,
×
152
                groupTemplate: t,
×
153
        }
×
154
        h.httpMux.Handle(pattern, limiterMatcher)
×
155
        return nil
×
156
}
157

158
// matcher is the HTTPHandle
159
type matcher struct {
160
        *HTTPLimiter
161
        groupTemplate *template.Template
162
}
163

164
func (h *HTTPLimiter) handleRequest(r *http.Request) error {
×
165
        if h.rewriteRequests {
×
166
                r = rest.RewriteForwardedRequest(r)
×
167
        }
×
168
        res, err := h.Reserve(r)
×
169
        if err != nil {
×
170
                return err
×
171
        }
×
172
        if res == nil || res.OK() {
×
173
                return nil
×
174
        } else {
×
175
                return &TooManyRequestsError{
×
176
                        Delay: res.Delay(),
×
177
                }
×
178
        }
×
179
}
180

181
func handleError(ctx context.Context, w http.ResponseWriter, err error) {
×
182
        var tooManyRequests *TooManyRequestsError
×
183
        status := http.StatusInternalServerError
×
184
        hdr := w.Header()
×
185
        hdr.Set("Content-Type", "application/json")
×
186
        if errors.As(err, &tooManyRequests) {
×
187
                status = http.StatusTooManyRequests
×
188
                retryAfter := int64(math.Ceil(tooManyRequests.Delay.Abs().Seconds()))
×
189
                hdr.Set("Retry-After", strconv.FormatInt(retryAfter, 10))
×
190
        } else {
×
191
                // Mask all internal error.
×
192
                // Caller must handle further processing (logging) of the underlying message.
×
193
                err = errors.New("internal error")
×
194
        }
×
195
        w.WriteHeader(status)
×
196
        b, _ := json.Marshal(rest.Error{
×
197
                Err:       err.Error(),
×
198
                RequestID: requestid.FromContext(ctx),
×
199
        })
×
200
        _, _ = w.Write(b)
×
201
}
202

203
// ServeHTTP implements a basic http.Handler so that handler can be used
204
// as a handler for the mux. It will only write on errors and is expected
205
// to continue to the actual handler on success.
206
func (h *HTTPLimiter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
×
207
        err := h.handleRequest(r)
×
208
        if err != nil {
×
209
                handleError(r.Context(), w, err)
×
210
        }
×
211
}
212

213
// MiddlewareGin implements rate limiting as a middleware for gin-gonic
214
// web framework.
215
func (h *HTTPLimiter) MiddlewareGin(c *gin.Context) {
×
216
        err := h.handleRequest(c.Request)
×
217
        if err != nil {
×
218
                _ = c.Error(err)
×
219
                handleError(c.Request.Context(), c.Writer, err)
×
220
                c.Abort()
×
221
        }
×
222
}
223

224
type okReservation struct{}
225

226
func (k okReservation) OK() bool             { return true }
×
227
func (k okReservation) Delay() time.Duration { return 0 }
×
228
func (k okReservation) Tokens() int64        { return math.MaxInt64 }
×
229

230
type rejectReservation struct{}
231

232
func (k rejectReservation) OK() bool             { return false }
×
233
func (k rejectReservation) Delay() time.Duration { return math.MaxInt64 }
×
234
func (k rejectReservation) Tokens() int64        { return -1 }
×
235

236
func (m *HTTPLimiter) Reserve(r *http.Request) (Reservation, error) {
×
237
        var b bytes.Buffer
×
238
        var eventLimiter *templateEventLimiter
×
239
        templateData := m.templateDataCallback(r)
×
240
        ctx := r.Context()
×
241
        h, _ := m.httpMux.Handler(r)
×
242
        hh, ok := h.(matcher)
×
243
        if ok && hh.groupTemplate != nil {
×
244
                err := hh.groupTemplate.Execute(&b, templateData)
×
245
                if err != nil {
×
246
                        return nil, fmt.Errorf("error executing ratelimit group template: %w", err)
×
247
                }
×
248
                eventLimiter = m.limiterGroups[b.String()]
×
249
                if eventLimiter == nil {
×
250
                        return m.fallbackReservation, nil
×
251
                }
×
252
                b.Reset()
×
253
        } else {
×
254
                return m.fallbackReservation, nil
×
255
        }
×
256
        err := eventLimiter.eventTemplate.Execute(&b, templateData)
×
257
        if err != nil {
×
258
                return nil, fmt.Errorf("error executing template for event ID: %w", err)
×
259
        }
×
260
        return eventLimiter.limiter.ReserveEvent(ctx, b.String())
×
261
}
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

© 2025 Coveralls, Inc