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

m-lab / locate / 21040898352

15 Jan 2026 05:49PM UTC coverage: 82.616% (-0.2%) from 82.781%
21040898352

Pull #238

github

robertodauria
add comment
Pull Request #238: feat(priority): implement tier-based rate limiting for /v2/priority/nearest

128 of 164 new or added lines in 4 files covered. (78.05%)

2 existing lines in 1 file now uncovered.

2381 of 2882 relevant lines covered (82.62%)

6.02 hits per line

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

91.67
/handler/handler.go
1
// Package handler provides a client and handlers for responding to locate
2
// requests.
3
package handler
4

5
import (
6
        "bytes"
7
        "context"
8
        "encoding/json"
9
        "errors"
10
        "fmt"
11
        "html/template"
12
        "math/rand"
13
        "net"
14
        "net/http"
15
        "net/url"
16
        "path"
17
        "strconv"
18
        "strings"
19
        "time"
20

21
        "github.com/go-jose/go-jose/v4/jwt"
22
        "github.com/google/uuid"
23
        log "github.com/sirupsen/logrus"
24

25
        "github.com/m-lab/go/rtx"
26
        v2 "github.com/m-lab/locate/api/v2"
27
        "github.com/m-lab/locate/clientgeo"
28
        "github.com/m-lab/locate/heartbeat"
29
        "github.com/m-lab/locate/limits"
30
        "github.com/m-lab/locate/metrics"
31
        "github.com/m-lab/locate/siteinfo"
32
        "github.com/m-lab/locate/static"
33
        prom "github.com/prometheus/client_golang/api/prometheus/v1"
34
        "github.com/prometheus/common/model"
35
)
36

37
var (
38
        errFailedToLookupClient = errors.New("Failed to look up client location")
39
        tooManyRequests         = "Too many periodic requests. Please contact support@measurementlab.net."
40
)
41

42
// Signer defines how access tokens are signed.
43
type Signer interface {
44
        Sign(cl jwt.Claims) (string, error)
45
}
46

47
type Limiter interface {
48
        IsLimited(ip, ua string) (limits.LimitStatus, error)
49
}
50

51
// TierLimiter extends Limiter with tier-based limits for priority endpoints.
52
type TierLimiter interface {
53
        Limiter
54
        IsLimitedWithTier(org, ip string, tierConfig limits.LimitConfig) (limits.LimitStatus, error)
55
}
56

57
// Client handles HTTP requests for the locate service, including nearest server
58
// lookups, heartbeat processing, and monitoring endpoints. It maintains dependencies
59
// for geolocation, rate limiting, access token signing, and metrics collection.
60
// TODO: This should probably be a Handler, not a Client.
61
type Client struct {
62
        Signer
63
        project string
64
        LocatorV2
65
        ClientLocator
66
        PrometheusClient
67
        targetTmpl       *template.Template
68
        agentLimits      limits.Agents
69
        tierLimits       limits.TierLimits
70
        ipLimiter        TierLimiter
71
        earlyExitClients map[string]bool
72
        jwtVerifier      Verifier
73
}
74

75
// LocatorV2 defines how the Nearest handler requests machines nearest to the
76
// client.
77
type LocatorV2 interface {
78
        Nearest(service string, lat, lon float64, opts *heartbeat.NearestOptions) (*heartbeat.TargetInfo, error)
79
        heartbeat.StatusTracker
80
}
81

82
// ClientLocator defines the interface for looking up the client geolocation.
83
type ClientLocator interface {
84
        Locate(req *http.Request) (*clientgeo.Location, error)
85
}
86

87
// PrometheusClient defines the interface to query Prometheus.
88
type PrometheusClient interface {
89
        Query(ctx context.Context, query string, ts time.Time, opts ...prom.Option) (model.Value, prom.Warnings, error)
90
}
91

92
type paramOpts struct {
93
        raw       url.Values
94
        version   string
95
        ranks     map[string]int
96
        svcParams map[string]float64
97
}
98

99
func init() {
1✔
100
        log.SetFormatter(&log.JSONFormatter{})
1✔
101
        log.SetLevel(log.InfoLevel)
1✔
102
}
1✔
103

104
// NewClient creates a new client.
105
func NewClient(project string, private Signer, locatorV2 LocatorV2, client ClientLocator,
106
        promClient PrometheusClient, lmts limits.Agents, tierLmts limits.TierLimits, limiter TierLimiter, earlyExitClients []string, jwtVerifier Verifier) *Client {
42✔
107
        // Convert slice to map for O(1) lookups
42✔
108
        earlyExitMap := make(map[string]bool)
42✔
109
        for _, client := range earlyExitClients {
42✔
110
                earlyExitMap[client] = true
×
111
        }
×
112
        return &Client{
42✔
113
                Signer:           private,
42✔
114
                project:          project,
42✔
115
                LocatorV2:        locatorV2,
42✔
116
                ClientLocator:    client,
42✔
117
                PrometheusClient: promClient,
42✔
118
                targetTmpl:       template.Must(template.New("name").Parse("{{.Hostname}}{{.Ports}}")),
42✔
119
                agentLimits:      lmts,
42✔
120
                tierLimits:       tierLmts,
42✔
121
                ipLimiter:        limiter,
42✔
122
                earlyExitClients: earlyExitMap,
42✔
123
                jwtVerifier:      jwtVerifier,
42✔
124
        }
42✔
125
}
126

127
// NewClientDirect creates a new client with a target template using only the target machine.
128
// TODO: Remove this and use NewClient in test code.
129
func NewClientDirect(project string, private Signer, locatorV2 LocatorV2, client ClientLocator, prom PrometheusClient) *Client {
1✔
130
        return &Client{
1✔
131
                Signer:           private,
1✔
132
                project:          project,
1✔
133
                LocatorV2:        locatorV2,
1✔
134
                ClientLocator:    client,
1✔
135
                PrometheusClient: prom,
1✔
136
                // Useful for the locatetest package when running a local server.
1✔
137
                targetTmpl: template.Must(template.New("name").Parse("{{.Hostname}}{{.Ports}}")),
1✔
138
        }
1✔
139
}
1✔
140

141
func (c *Client) extraParams(hostname string, index int, p paramOpts) url.Values {
19✔
142
        v := url.Values{}
19✔
143

19✔
144
        // Add client parameters.
19✔
145
        for key := range p.raw {
34✔
146
                if strings.HasPrefix(key, "client_") {
24✔
147
                        // note: we only use the first value.
9✔
148
                        v.Set(key, p.raw.Get(key))
9✔
149
                }
9✔
150

151
                val, ok := p.svcParams[key]
15✔
152
                if ok && rand.Float64() < val {
20✔
153
                        v.Set(key, p.raw.Get(key))
5✔
154
                }
5✔
155
        }
156

157
        // Add early_exit parameter for specified clients
158
        clientName := p.raw.Get("client_name")
19✔
159
        if clientName != "" && c.earlyExitClients[clientName] {
20✔
160
                v.Set(static.EarlyExitParameter, static.EarlyExitDefaultValue)
1✔
161
        }
1✔
162

163
        // Add Locate Service version.
164
        v.Set("locate_version", p.version)
19✔
165

19✔
166
        // Add metro rank.
19✔
167
        rank, ok := p.ranks[hostname]
19✔
168
        if ok {
23✔
169
                v.Set("metro_rank", strconv.Itoa(rank))
4✔
170
        }
4✔
171

172
        // Add result index.
173
        v.Set("index", strconv.Itoa(index))
19✔
174

19✔
175
        return v
19✔
176
}
177

178
// Nearest uses an implementation of the LocatorV2 interface to look up
179
// nearest servers.
180
func (c *Client) Nearest(rw http.ResponseWriter, req *http.Request) {
14✔
181
        req.ParseForm()
14✔
182
        result := v2.NearestResult{}
14✔
183
        setHeaders(rw)
14✔
184

14✔
185
        if c.limitRequest(time.Now().UTC(), req) {
15✔
186
                result.Error = v2.NewError("client", tooManyRequests, http.StatusTooManyRequests)
1✔
187
                writeResult(rw, result.Error.Status, &result)
1✔
188
                metrics.RequestsTotal.WithLabelValues("nearest", "request limit", http.StatusText(result.Error.Status)).Inc()
1✔
189
                return
1✔
190
        }
1✔
191

192
        // Check rate limit for IP and UA.
193
        if c.ipLimiter != nil {
18✔
194
                ip := getRemoteAddr(req)
5✔
195
                if ip != "" {
10✔
196
                        // An empty UA is technically possible.
5✔
197
                        ua := req.Header.Get("User-Agent")
5✔
198
                        status, err := c.ipLimiter.IsLimited(ip, ua)
5✔
199
                        if err != nil {
6✔
200
                                // Log error but don't block request (fail open).
1✔
201
                                // TODO: Add tests for this path.
1✔
202
                                log.Printf("Rate limiter error: %v", err)
1✔
203
                        } else if status.IsLimited {
7✔
204
                                // Log IP and UA and block the request.
2✔
205
                                result.Error = v2.NewError("client", tooManyRequests, http.StatusTooManyRequests)
2✔
206
                                metrics.RequestsTotal.WithLabelValues("nearest", "rate limit",
2✔
207
                                        http.StatusText(result.Error.Status)).Inc()
2✔
208
                                // If the client provided a client_name, we want to know how many times
2✔
209
                                // that client_name was rate limited. This may be empty, which is fine.
2✔
210
                                clientName := req.Form.Get("client_name")
2✔
211
                                metrics.RateLimitedTotal.WithLabelValues(clientName, status.LimitType).Inc()
2✔
212

2✔
213
                                log.Printf("Rate limit (%s) exceeded for IP: %s, client: %s, UA: %s", ip,
2✔
214
                                        status.LimitType, clientName, ua)
2✔
215
                                writeResult(rw, result.Error.Status, &result)
2✔
216
                                return
2✔
217
                        }
2✔
218
                } else {
×
219
                        // This should never happen if Locate is deployed on AppEngine.
×
220
                        log.Println("Cannot find IP address for rate limiting.")
×
221
                }
×
222
        }
223

224
        c.handleNearestRequest(rw, req, &result, "nearest")
11✔
225
}
226

227
// handleNearestRequest is a helper that contains the common logic for looking up
228
// nearest machines and generating the response. It's used by both Nearest and
229
// PriorityNearest handlers.
230
func (c *Client) handleNearestRequest(rw http.ResponseWriter, req *http.Request, result *v2.NearestResult, metricLabel string) {
15✔
231
        experiment, service := getExperimentAndService(req.URL.Path)
15✔
232

15✔
233
        // Look up client location.
15✔
234
        loc, err := c.checkClientLocation(rw, req)
15✔
235
        if err != nil {
17✔
236
                status := http.StatusServiceUnavailable
2✔
237
                result.Error = v2.NewError("nearest", "Failed to lookup nearest machines", status)
2✔
238
                writeResult(rw, result.Error.Status, result)
2✔
239
                metrics.RequestsTotal.WithLabelValues(metricLabel, "client location",
2✔
240
                        http.StatusText(result.Error.Status)).Inc()
2✔
241
                return
2✔
242
        }
2✔
243

244
        // Parse client location.
245
        lat, errLat := strconv.ParseFloat(loc.Latitude, 64)
13✔
246
        lon, errLon := strconv.ParseFloat(loc.Longitude, 64)
13✔
247
        if errLat != nil || errLon != nil {
14✔
248
                result.Error = v2.NewError("client", errFailedToLookupClient.Error(), http.StatusInternalServerError)
1✔
249
                writeResult(rw, result.Error.Status, result)
1✔
250
                metrics.RequestsTotal.WithLabelValues(metricLabel, "parse client location",
1✔
251
                        http.StatusText(result.Error.Status)).Inc()
1✔
252
                return
1✔
253
        }
1✔
254

255
        // Find the nearest targets using the client parameters.
256
        q := req.URL.Query()
12✔
257
        t := q.Get("machine-type")
12✔
258
        country := req.Header.Get("X-AppEngine-Country")
12✔
259
        sites := q["site"]
12✔
260
        org := q.Get("org")
12✔
261
        strict := false
12✔
262
        if qsStrict, err := strconv.ParseBool(q.Get("strict")); err == nil {
12✔
263
                strict = qsStrict
×
UNCOV
264
        }
×
265
        // If strict, override the country from the AppEngine header with the one in
266
        // the querystring.
267
        if strict {
12✔
268
                country = q.Get("country")
×
269
        }
×
270
        opts := &heartbeat.NearestOptions{Type: t, Country: country, Sites: sites, Org: org, Strict: strict}
12✔
271
        targetInfo, err := c.LocatorV2.Nearest(service, lat, lon, opts)
12✔
272
        if err != nil {
14✔
273
                result.Error = v2.NewError("nearest", "Failed to lookup nearest machines", http.StatusInternalServerError)
2✔
274
                writeResult(rw, result.Error.Status, result)
2✔
275
                metrics.RequestsTotal.WithLabelValues(metricLabel, "server location",
2✔
276
                        http.StatusText(result.Error.Status)).Inc()
2✔
277
                return
2✔
278
        }
2✔
279

280
        pOpts := paramOpts{
10✔
281
                raw:       req.Form,
10✔
282
                version:   "v2",
10✔
283
                ranks:     targetInfo.Ranks,
10✔
284
                svcParams: static.ServiceParams,
10✔
285
        }
10✔
286
        // Populate target URLs and write out response.
10✔
287
        c.populateURLs(targetInfo.Targets, targetInfo.URLs, experiment, pOpts)
10✔
288
        result.Results = targetInfo.Targets
10✔
289
        writeResult(rw, http.StatusOK, result)
10✔
290
        metrics.RequestsTotal.WithLabelValues(metricLabel, "success", http.StatusText(http.StatusOK)).Inc()
10✔
291
}
292

293
// PriorityNearest handles requests to /v2/priority/nearest with tier-based rate limiting.
294
// It requires a valid integration JWT with an int_id claim. Requests without valid
295
// credentials receive a 401 Unauthorized response.
296
func (c *Client) PriorityNearest(rw http.ResponseWriter, req *http.Request) {
7✔
297
        if err := req.ParseForm(); err != nil {
7✔
NEW
298
                log.Debugf("ParseForm error: %v", err)
×
NEW
299
        }
×
300
        result := v2.NearestResult{}
7✔
301
        setHeaders(rw)
7✔
302

7✔
303
        // Extract JWT claims from the X-Endpoint-API-UserInfo header
7✔
304
        claims, err := c.extractJWTClaims(req)
7✔
305
        if err != nil {
8✔
306
                log.Debugf("Failed to extract JWT claims for priority endpoint: %v", err)
1✔
307
                result.Error = v2.NewError("auth", "Valid JWT token required", http.StatusUnauthorized)
1✔
308
                writeResult(rw, result.Error.Status, &result)
1✔
309
                metrics.RequestsTotal.WithLabelValues("priority_nearest", "auth", http.StatusText(http.StatusUnauthorized)).Inc()
1✔
310
                return
1✔
311
        }
1✔
312

313
        // Extract integration ID claim
314
        intID, err := extractIntegrationID(claims)
6✔
315
        if err != nil {
7✔
316
                log.Debugf("Failed to extract int_id claim for priority endpoint: %v", err)
1✔
317
                result.Error = v2.NewError("auth", "Valid int_id claim required", http.StatusUnauthorized)
1✔
318
                writeResult(rw, result.Error.Status, &result)
1✔
319
                metrics.RequestsTotal.WithLabelValues("priority_nearest", "auth", http.StatusText(http.StatusUnauthorized)).Inc()
1✔
320
                return
1✔
321
        }
1✔
322

323
        // Extract tier claim, defaulting to 0 if not present.
324
        // JSON numbers always unmarshal to float64 in Go.
325
        tier := 0
5✔
326
        if tierClaim, ok := claims["tier"]; ok {
9✔
327
                if v, ok := tierClaim.(float64); ok {
7✔
328
                        tier = int(v)
3✔
329
                } else {
4✔
330
                        log.Debugf("Unexpected tier claim type for int_id %s: %T, using default tier 0", intID, tierClaim)
1✔
331
                }
1✔
332
        }
333

334
        // Look up tier configuration
335
        tierConfig, ok := c.tierLimits[tier]
5✔
336
        if !ok {
6✔
337
                log.Debugf("Tier %d not configured for int_id %s, using tier 0", tier, intID)
1✔
338
                tier = 0
1✔
339
                tierConfig, ok = c.tierLimits[0]
1✔
340
                if !ok {
1✔
NEW
341
                        // Tier 0 not configured - this is a server misconfiguration
×
NEW
342
                        log.Errorf("Tier 0 not configured, cannot serve priority requests")
×
NEW
343
                        result.Error = v2.NewError("server", "Service misconfigured", http.StatusInternalServerError)
×
NEW
344
                        writeResult(rw, result.Error.Status, &result)
×
NEW
345
                        metrics.RequestsTotal.WithLabelValues("priority_nearest", "config", http.StatusText(http.StatusInternalServerError)).Inc()
×
NEW
346
                        return
×
NEW
347
                }
×
348
        }
349

350
        // Apply tier-based rate limiting
351
        if c.ipLimiter != nil {
10✔
352
                ip := getRemoteAddr(req)
5✔
353
                if ip != "" {
10✔
354
                        status, err := c.ipLimiter.IsLimitedWithTier(intID, ip, tierConfig)
5✔
355
                        if err != nil {
5✔
NEW
356
                                // Log error but don't block request (fail open).
×
NEW
357
                                log.Errorf("Tier rate limiter error for int_id %s, tier %d: %v", intID, tier, err)
×
358
                        } else if status.IsLimited {
6✔
359
                                // Rate limited - block the request
1✔
360
                                result.Error = v2.NewError("client", tooManyRequests, http.StatusTooManyRequests)
1✔
361
                                metrics.RequestsTotal.WithLabelValues("priority_nearest", "rate limit",
1✔
362
                                        http.StatusText(result.Error.Status)).Inc()
1✔
363

1✔
364
                                clientName := req.Form.Get("client_name")
1✔
365
                                metrics.RateLimitedTotal.WithLabelValues(clientName, status.LimitType).Inc()
1✔
366

1✔
367
                                log.Debugf("Tier rate limit (%s) exceeded for int_id: %s, tier: %d, IP: %s, client: %s",
1✔
368
                                        status.LimitType, intID, tier, ip, clientName)
1✔
369
                                writeResult(rw, result.Error.Status, &result)
1✔
370
                                return
1✔
371
                        }
1✔
NEW
372
                } else {
×
NEW
373
                        // This should never happen if Locate is deployed on AppEngine.
×
NEW
374
                        log.Warnf("Cannot find IP address for tier-based rate limiting.")
×
NEW
375
                }
×
376
        }
377

378
        // Rate limit passed - handle the nearest request
379
        c.handleNearestRequest(rw, req, &result, "priority_nearest")
4✔
380
}
381

382
// extractIntegrationID extracts the "int_id" claim from integration JWT claims.
383
func extractIntegrationID(claims map[string]interface{}) (string, error) {
6✔
384
        intIDClaim, ok := claims["int_id"]
6✔
385
        if !ok {
7✔
386
                return "", fmt.Errorf("int_id claim not found in JWT")
1✔
387
        }
1✔
388

389
        intID, ok := intIDClaim.(string)
5✔
390
        if !ok {
5✔
NEW
391
                return "", fmt.Errorf("int_id claim is not a string")
×
NEW
392
        }
×
393

394
        if intID == "" {
5✔
NEW
395
                return "", fmt.Errorf("int_id claim is empty")
×
NEW
396
        }
×
397

398
        return intID, nil
5✔
399
}
400

401
// Live is a minimal handler to indicate that the server is operating at all.
402
func (c *Client) Live(rw http.ResponseWriter, req *http.Request) {
2✔
403
        fmt.Fprintf(rw, "ok")
2✔
404
}
2✔
405

406
// Ready reports whether the server is working as expected and ready to serve requests.
407
func (c *Client) Ready(rw http.ResponseWriter, req *http.Request) {
2✔
408
        if c.LocatorV2.Ready() {
3✔
409
                fmt.Fprintf(rw, "ok")
1✔
410
        } else {
2✔
411
                rw.WriteHeader(http.StatusInternalServerError)
1✔
412
                fmt.Fprintf(rw, "not ready")
1✔
413
        }
1✔
414
}
415

416
// Registrations returns information about registered machines. There are 3
417
// supported query parameters:
418
//
419
// * format - defines the format of the returned JSON
420
// * org - limits results to only records for the given organization
421
// * exp - limits results to only records for the given experiment (e.g., ndt)
422
//
423
// The "org" and "exp" query parameters are currently only supported by the
424
// default or "machines" format.
425
func (c *Client) Registrations(rw http.ResponseWriter, req *http.Request) {
2✔
426
        var err error
2✔
427
        var result interface{}
2✔
428

2✔
429
        setHeaders(rw)
2✔
430

2✔
431
        q := req.URL.Query()
2✔
432
        format := q.Get("format")
2✔
433

2✔
434
        switch format {
2✔
UNCOV
435
        case "geo":
×
436
                result, err = siteinfo.Geo(c.LocatorV2.Instances(), q)
×
437
        default:
2✔
438
                result, err = siteinfo.Hosts(c.LocatorV2.Instances(), q)
2✔
439
        }
440

441
        if err != nil {
3✔
442
                v2Error := v2.NewError("siteinfo", err.Error(), http.StatusInternalServerError)
1✔
443
                writeResult(rw, http.StatusInternalServerError, v2Error)
1✔
444
                return
1✔
445
        }
1✔
446

447
        writeResult(rw, http.StatusOK, result)
1✔
448
}
449

450
// checkClientLocation looks up the client location and copies the location
451
// headers to the response writer.
452
func (c *Client) checkClientLocation(rw http.ResponseWriter, req *http.Request) (*clientgeo.Location, error) {
15✔
453
        // Lookup the client location using the client request.
15✔
454
        loc, err := c.Locate(req)
15✔
455
        if err != nil {
17✔
456
                return nil, errFailedToLookupClient
2✔
457
        }
2✔
458

459
        // Copy location headers to response writer.
460
        for key := range loc.Headers {
37✔
461
                rw.Header().Set(key, loc.Headers.Get(key))
24✔
462
        }
24✔
463

464
        return loc, nil
13✔
465
}
466

467
// populateURLs populates each set of URLs using the target configuration.
468
func (c *Client) populateURLs(targets []v2.Target, ports static.Ports, exp string, pOpts paramOpts) {
10✔
469
        for i, target := range targets {
20✔
470
                token := c.getAccessToken(target.Machine, exp)
10✔
471
                params := c.extraParams(target.Machine, i, pOpts)
10✔
472
                targets[i].URLs = c.getURLs(ports, target.Hostname, token, params)
10✔
473
        }
10✔
474
}
475

476
// getAccessToken allocates a new access token using the given machine name as
477
// the intended audience and the subject as the target service.
478
func (c *Client) getAccessToken(machine, subject string) string {
11✔
479
        // Create the token. The same access token is reused for every URL of a
11✔
480
        // target port.
11✔
481
        // A uuid is added to the claims so that each new token is unique.
11✔
482
        cl := jwt.Claims{
11✔
483
                Issuer:   static.IssuerLocate,
11✔
484
                Subject:  subject,
11✔
485
                Audience: jwt.Audience{machine},
11✔
486
                Expiry:   jwt.NewNumericDate(time.Now().Add(time.Minute)),
11✔
487
                ID:       uuid.NewString(),
11✔
488
        }
11✔
489
        token, err := c.Sign(cl)
11✔
490
        // Sign errors can only happen due to a misconfiguration of the key.
11✔
491
        // A good config will remain good.
11✔
492
        rtx.PanicOnError(err, "signing claims has failed")
11✔
493
        return token
11✔
494
}
11✔
495

496
// getURLs creates URLs for the named experiment, running on the named machine
497
// for each given port. Every URL will include an `access_token=` parameter,
498
// authorizing the measurement.
499
func (c *Client) getURLs(ports static.Ports, hostname, token string, extra url.Values) map[string]string {
11✔
500
        urls := map[string]string{}
11✔
501
        // For each port config, prepare the target url with access_token and
11✔
502
        // complete host field.
11✔
503
        for _, target := range ports {
29✔
504
                name := target.String()
18✔
505
                params := url.Values{}
18✔
506
                params.Set("access_token", token)
18✔
507
                for key := range extra {
62✔
508
                        // note: we only use the first value.
44✔
509
                        params.Set(key, extra.Get(key))
44✔
510
                }
44✔
511
                target.RawQuery = params.Encode()
18✔
512

18✔
513
                host := &bytes.Buffer{}
18✔
514
                err := c.targetTmpl.Execute(host, map[string]string{
18✔
515
                        "Hostname": hostname,
18✔
516
                        "Ports":    target.Host, // from URL template, so typically just the ":port".
18✔
517
                })
18✔
518
                rtx.PanicOnError(err, "bad template evaluation")
18✔
519
                target.Host = host.String()
18✔
520
                urls[name] = target.String()
18✔
521
        }
522
        return urls
11✔
523
}
524

525
// limitRequest determines whether a client request should be rate-limited.
526
func (c *Client) limitRequest(now time.Time, req *http.Request) bool {
18✔
527
        agent := req.Header.Get("User-Agent")
18✔
528
        l, ok := c.agentLimits[agent]
18✔
529
        if !ok {
33✔
530
                // No limit defined for user agent.
15✔
531
                return false
15✔
532
        }
15✔
533
        return l.IsLimited(now)
3✔
534
}
535

536
// setHeaders sets the response headers for "nearest" requests.
537
func setHeaders(rw http.ResponseWriter) {
23✔
538
        // Set CORS policy to allow third-party websites to use returned resources.
23✔
539
        rw.Header().Set("Content-Type", "application/json")
23✔
540
        rw.Header().Set("Access-Control-Allow-Origin", "*")
23✔
541
        // Prevent caching of result.
23✔
542
        // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
23✔
543
        rw.Header().Set("Cache-Control", "no-store")
23✔
544
}
23✔
545

546
// writeResult marshals the result and writes the result to the response writer.
547
func writeResult(rw http.ResponseWriter, status int, result interface{}) {
27✔
548
        b, err := json.MarshalIndent(result, "", "  ")
27✔
549
        // Errors are only possible when marshalling incompatible types, like functions.
27✔
550
        rtx.PanicOnError(err, "Failed to format result")
27✔
551
        rw.WriteHeader(status)
27✔
552
        rw.Write(b)
27✔
553
}
27✔
554

555
// getExperimentAndService takes an http request path and extracts the last two
556
// fields. For correct requests (e.g. "/v2/nearest/ndt/ndt5"), this will be the
557
// experiment name (e.g. "ndt") and the datatype (e.g. "ndt5").
558
func getExperimentAndService(p string) (string, string) {
17✔
559
        datatype := path.Base(p)
17✔
560
        experiment := path.Base(path.Dir(p))
17✔
561
        return experiment, experiment + "/" + datatype
17✔
562
}
17✔
563

564
// getRemoteAddr extracts the remote address from the request. When running on
565
// Google App Engine, the X-Forwarded-For is guaranteed to be set. The GCP load
566
// balancer appends the actual client IP and the load balancer IP to any existing
567
// X-Forwarded-For header, so the second-to-last IP is the real client address.
568
// When running elsewhere (including on the local machine), the RemoteAddr from
569
// the request is used instead.
570
func getRemoteAddr(req *http.Request) string {
17✔
571
        xff := req.Header.Get("X-Forwarded-For")
17✔
572
        if xff != "" {
31✔
573
                // Split by comma and trim spaces from each IP
14✔
574
                ips := strings.Split(xff, ",")
14✔
575
                for i := range ips {
37✔
576
                        ips[i] = strings.TrimSpace(ips[i])
23✔
577
                }
23✔
578

579
                // GCP load balancer appends: <original-header>, <client-ip>, <lb-ip>
580
                // The second-to-last IP is the actual client address
581
                if len(ips) >= 2 {
18✔
582
                        return ips[len(ips)-2]
4✔
583
                } else if len(ips) == 1 && ips[0] != "" {
24✔
584
                        return ips[0]
10✔
585
                }
10✔
586
        }
587

588
        // Fall back to RemoteAddr for local testing or deployments outside of GAE.
589
        host, _, err := net.SplitHostPort(req.RemoteAddr)
3✔
590
        if err != nil {
4✔
591
                // As a last resort, use the whole RemoteAddr.
1✔
592
                return strings.TrimSpace(req.RemoteAddr)
1✔
593
        }
1✔
594
        return host
2✔
595
}
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