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

m-lab / locate / 21041445014

15 Jan 2026 06:09PM UTC coverage: 82.323% (-0.5%) from 82.781%
21041445014

Pull #238

github

robertodauria
fix: handle ParseForm errors in Nearest() too
Pull Request #238: feat(priority): implement tier-based rate limiting for /v2/priority/nearest

137 of 185 new or added lines in 4 files covered. (74.05%)

21 existing lines in 1 file now uncovered.

2389 of 2902 relevant lines covered (82.32%)

6.02 hits per line

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

88.77
/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
        result := v2.NearestResult{}
14✔
182
        setHeaders(rw)
14✔
183

14✔
184
        if err := req.ParseForm(); err != nil {
14✔
NEW
UNCOV
185
                log.Debugf("ParseForm error: %v", err)
×
NEW
UNCOV
186
                result.Error = v2.NewError("server", "Failed to parse form", http.StatusInternalServerError)
×
NEW
UNCOV
187
                writeResult(rw, result.Error.Status, result)
×
NEW
UNCOV
188
                metrics.RequestsTotal.WithLabelValues("nearest", "server",
×
NEW
UNCOV
189
                        http.StatusText(result.Error.Status)).Inc()
×
NEW
UNCOV
190
                return
×
NEW
UNCOV
191
        }
×
192

193
        if c.limitRequest(time.Now().UTC(), req) {
15✔
194
                result.Error = v2.NewError("client", tooManyRequests, http.StatusTooManyRequests)
1✔
195
                writeResult(rw, result.Error.Status, &result)
1✔
196
                metrics.RequestsTotal.WithLabelValues("nearest", "request limit", http.StatusText(result.Error.Status)).Inc()
1✔
197
                return
1✔
198
        }
1✔
199

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

2✔
221
                                log.Printf("Rate limit (%s) exceeded for IP: %s, client: %s, UA: %s", ip,
2✔
222
                                        status.LimitType, clientName, ua)
2✔
223
                                writeResult(rw, result.Error.Status, &result)
2✔
224
                                return
2✔
225
                        }
2✔
UNCOV
226
                } else {
×
UNCOV
227
                        // This should never happen if Locate is deployed on AppEngine.
×
UNCOV
228
                        log.Println("Cannot find IP address for rate limiting.")
×
UNCOV
229
                }
×
230
        }
231

232
        c.handleNearestRequest(rw, req, &result, "nearest")
11✔
233
}
234

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

15✔
241
        // Look up client location.
15✔
242
        loc, err := c.checkClientLocation(rw, req)
15✔
243
        if err != nil {
17✔
244
                status := http.StatusServiceUnavailable
2✔
245
                result.Error = v2.NewError("nearest", "Failed to lookup nearest machines", status)
2✔
246
                writeResult(rw, result.Error.Status, result)
2✔
247
                metrics.RequestsTotal.WithLabelValues(metricLabel, "client location",
2✔
248
                        http.StatusText(result.Error.Status)).Inc()
2✔
249
                return
2✔
250
        }
2✔
251

252
        // Parse client location.
253
        lat, errLat := strconv.ParseFloat(loc.Latitude, 64)
13✔
254
        lon, errLon := strconv.ParseFloat(loc.Longitude, 64)
13✔
255
        if errLat != nil || errLon != nil {
14✔
256
                result.Error = v2.NewError("client", errFailedToLookupClient.Error(), http.StatusInternalServerError)
1✔
257
                writeResult(rw, result.Error.Status, result)
1✔
258
                metrics.RequestsTotal.WithLabelValues(metricLabel, "parse client location",
1✔
259
                        http.StatusText(result.Error.Status)).Inc()
1✔
260
                return
1✔
261
        }
1✔
262

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

288
        pOpts := paramOpts{
10✔
289
                raw:       req.Form,
10✔
290
                version:   "v2",
10✔
291
                ranks:     targetInfo.Ranks,
10✔
292
                svcParams: static.ServiceParams,
10✔
293
        }
10✔
294
        // Populate target URLs and write out response.
10✔
295
        c.populateURLs(targetInfo.Targets, targetInfo.URLs, experiment, pOpts)
10✔
296
        result.Results = targetInfo.Targets
10✔
297
        writeResult(rw, http.StatusOK, result)
10✔
298
        metrics.RequestsTotal.WithLabelValues(metricLabel, "success", http.StatusText(http.StatusOK)).Inc()
10✔
299
}
300

301
// PriorityNearest handles requests to /v2/priority/nearest with tier-based rate limiting.
302
// It requires a valid integration JWT with an int_id claim. Requests without valid
303
// credentials receive a 401 Unauthorized response.
304
func (c *Client) PriorityNearest(rw http.ResponseWriter, req *http.Request) {
7✔
305
        result := &v2.NearestResult{}
7✔
306
        setHeaders(rw)
7✔
307

7✔
308
        if err := req.ParseForm(); err != nil {
7✔
NEW
309
                log.Debugf("ParseForm error: %v", err)
×
NEW
310
                result.Error = v2.NewError("server", "Failed to parse form", http.StatusInternalServerError)
×
NEW
311
                writeResult(rw, result.Error.Status, result)
×
NEW
312
                metrics.RequestsTotal.WithLabelValues("priority_nearest", "server",
×
NEW
313
                        http.StatusText(result.Error.Status)).Inc()
×
NEW
314
                return
×
NEW
315
        }
×
316

317
        // Extract JWT claims from thee X-Endpoint-API-UserInfo header
318
        claims, err := c.extractJWTClaims(req)
7✔
319
        if err != nil {
8✔
320
                log.Debugf("Failed to extract JWT claims for priority endpoint: %v", err)
1✔
321
                result.Error = v2.NewError("auth", "Valid JWT token required", http.StatusUnauthorized)
1✔
322
                writeResult(rw, result.Error.Status, result)
1✔
323
                metrics.RequestsTotal.WithLabelValues("priority_nearest", "auth", http.StatusText(http.StatusUnauthorized)).Inc()
1✔
324
                return
1✔
325
        }
1✔
326

327
        // Extract integration ID claim
328
        intID, err := extractIntegrationID(claims)
6✔
329
        if err != nil {
7✔
330
                log.Debugf("Failed to extract int_id claim for priority endpoint: %v", err)
1✔
331
                result.Error = v2.NewError("auth", "Valid int_id claim required", http.StatusUnauthorized)
1✔
332
                writeResult(rw, result.Error.Status, result)
1✔
333
                metrics.RequestsTotal.WithLabelValues("priority_nearest", "auth", http.StatusText(http.StatusUnauthorized)).Inc()
1✔
334
                return
1✔
335
        }
1✔
336

337
        // Extract tier claim, defaulting to 0 if not present.
338
        // JSON numbers always unmarshal to float64 in Go.
339
        tier := 0
5✔
340
        if tierClaim, ok := claims["tier"]; ok {
9✔
341
                if v, ok := tierClaim.(float64); ok {
7✔
342
                        tier = int(v)
3✔
343
                } else {
4✔
344
                        log.Debugf("Unexpected tier claim type for int_id %s: %T, using default tier 0", intID, tierClaim)
1✔
345
                }
1✔
346
        }
347

348
        // Look up tier configuration
349
        tierConfig, ok := c.tierLimits[tier]
5✔
350
        if !ok {
6✔
351
                log.Debugf("Tier %d not configured for int_id %s, using tier 0", tier, intID)
1✔
352
                tier = 0
1✔
353
                tierConfig, ok = c.tierLimits[0]
1✔
354
                if !ok {
1✔
NEW
355
                        // Tier 0 not configured - this is a server misconfiguration
×
NEW
356
                        log.Errorf("Tier 0 not configured, cannot serve priority requests")
×
NEW
357
                        result.Error = v2.NewError("server", "Service misconfigured", http.StatusInternalServerError)
×
NEW
358
                        writeResult(rw, result.Error.Status, result)
×
NEW
359
                        metrics.RequestsTotal.WithLabelValues("priority_nearest", "config", http.StatusText(http.StatusInternalServerError)).Inc()
×
NEW
360
                        return
×
NEW
361
                }
×
362
        }
363

364
        // Apply tier-based rate limiting
365
        if c.ipLimiter != nil {
10✔
366
                ip := getRemoteAddr(req)
5✔
367
                if ip != "" {
10✔
368
                        status, err := c.ipLimiter.IsLimitedWithTier(intID, ip, tierConfig)
5✔
369
                        if err != nil {
5✔
NEW
370
                                // Log error but don't block request (fail open).
×
NEW
371
                                log.Errorf("Tier rate limiter error for int_id %s, tier %d: %v", intID, tier, err)
×
372
                        } else if status.IsLimited {
6✔
373
                                // Rate limited - block the request
1✔
374
                                result.Error = v2.NewError("client", tooManyRequests, http.StatusTooManyRequests)
1✔
375
                                metrics.RequestsTotal.WithLabelValues("priority_nearest", "rate limit",
1✔
376
                                        http.StatusText(result.Error.Status)).Inc()
1✔
377

1✔
378
                                clientName := req.Form.Get("client_name")
1✔
379
                                metrics.RateLimitedTotal.WithLabelValues(clientName, status.LimitType).Inc()
1✔
380

1✔
381
                                log.Debugf("Tier rate limit (%s) exceeded for int_id: %s, tier: %d, IP: %s, client: %s",
1✔
382
                                        status.LimitType, intID, tier, ip, clientName)
1✔
383
                                writeResult(rw, result.Error.Status, result)
1✔
384
                                return
1✔
385
                        }
1✔
NEW
386
                } else {
×
NEW
387
                        // This should never happen if Locate is deployed on AppEngine.
×
NEW
388
                        log.Warnf("Cannot find IP address for tier-based rate limiting.")
×
NEW
389
                }
×
390
        }
391

392
        // Rate limit passed - handle the nearest request
393
        c.handleNearestRequest(rw, req, result, "priority_nearest")
4✔
394
}
395

396
// extractIntegrationID extracts the "int_id" claim from integration JWT claims.
397
func extractIntegrationID(claims map[string]interface{}) (string, error) {
6✔
398
        intIDClaim, ok := claims["int_id"]
6✔
399
        if !ok {
7✔
400
                return "", fmt.Errorf("int_id claim not found in JWT")
1✔
401
        }
1✔
402

403
        intID, ok := intIDClaim.(string)
5✔
404
        if !ok {
5✔
NEW
UNCOV
405
                return "", fmt.Errorf("int_id claim is not a string")
×
NEW
UNCOV
406
        }
×
407

408
        if intID == "" {
5✔
NEW
UNCOV
409
                return "", fmt.Errorf("int_id claim is empty")
×
NEW
UNCOV
410
        }
×
411

412
        return intID, nil
5✔
413
}
414

415
// Live is a minimal handler to indicate that the server is operating at all.
416
func (c *Client) Live(rw http.ResponseWriter, req *http.Request) {
2✔
417
        fmt.Fprintf(rw, "ok")
2✔
418
}
2✔
419

420
// Ready reports whether the server is working as expected and ready to serve requests.
421
func (c *Client) Ready(rw http.ResponseWriter, req *http.Request) {
2✔
422
        if c.LocatorV2.Ready() {
3✔
423
                fmt.Fprintf(rw, "ok")
1✔
424
        } else {
2✔
425
                rw.WriteHeader(http.StatusInternalServerError)
1✔
426
                fmt.Fprintf(rw, "not ready")
1✔
427
        }
1✔
428
}
429

430
// Registrations returns information about registered machines. There are 3
431
// supported query parameters:
432
//
433
// * format - defines the format of the returned JSON
434
// * org - limits results to only records for the given organization
435
// * exp - limits results to only records for the given experiment (e.g., ndt)
436
//
437
// The "org" and "exp" query parameters are currently only supported by the
438
// default or "machines" format.
439
func (c *Client) Registrations(rw http.ResponseWriter, req *http.Request) {
2✔
440
        var err error
2✔
441
        var result interface{}
2✔
442

2✔
443
        setHeaders(rw)
2✔
444

2✔
445
        q := req.URL.Query()
2✔
446
        format := q.Get("format")
2✔
447

2✔
448
        switch format {
2✔
UNCOV
449
        case "geo":
×
UNCOV
450
                result, err = siteinfo.Geo(c.LocatorV2.Instances(), q)
×
451
        default:
2✔
452
                result, err = siteinfo.Hosts(c.LocatorV2.Instances(), q)
2✔
453
        }
454

455
        if err != nil {
3✔
456
                v2Error := v2.NewError("siteinfo", err.Error(), http.StatusInternalServerError)
1✔
457
                writeResult(rw, http.StatusInternalServerError, v2Error)
1✔
458
                return
1✔
459
        }
1✔
460

461
        writeResult(rw, http.StatusOK, result)
1✔
462
}
463

464
// checkClientLocation looks up the client location and copies the location
465
// headers to the response writer.
466
func (c *Client) checkClientLocation(rw http.ResponseWriter, req *http.Request) (*clientgeo.Location, error) {
15✔
467
        // Lookup the client location using the client request.
15✔
468
        loc, err := c.Locate(req)
15✔
469
        if err != nil {
17✔
470
                return nil, errFailedToLookupClient
2✔
471
        }
2✔
472

473
        // Copy location headers to response writer.
474
        for key := range loc.Headers {
37✔
475
                rw.Header().Set(key, loc.Headers.Get(key))
24✔
476
        }
24✔
477

478
        return loc, nil
13✔
479
}
480

481
// populateURLs populates each set of URLs using the target configuration.
482
func (c *Client) populateURLs(targets []v2.Target, ports static.Ports, exp string, pOpts paramOpts) {
10✔
483
        for i, target := range targets {
20✔
484
                token := c.getAccessToken(target.Machine, exp)
10✔
485
                params := c.extraParams(target.Machine, i, pOpts)
10✔
486
                targets[i].URLs = c.getURLs(ports, target.Hostname, token, params)
10✔
487
        }
10✔
488
}
489

490
// getAccessToken allocates a new access token using the given machine name as
491
// the intended audience and the subject as the target service.
492
func (c *Client) getAccessToken(machine, subject string) string {
11✔
493
        // Create the token. The same access token is reused for every URL of a
11✔
494
        // target port.
11✔
495
        // A uuid is added to the claims so that each new token is unique.
11✔
496
        cl := jwt.Claims{
11✔
497
                Issuer:   static.IssuerLocate,
11✔
498
                Subject:  subject,
11✔
499
                Audience: jwt.Audience{machine},
11✔
500
                Expiry:   jwt.NewNumericDate(time.Now().Add(time.Minute)),
11✔
501
                ID:       uuid.NewString(),
11✔
502
        }
11✔
503
        token, err := c.Sign(cl)
11✔
504
        // Sign errors can only happen due to a misconfiguration of the key.
11✔
505
        // A good config will remain good.
11✔
506
        rtx.PanicOnError(err, "signing claims has failed")
11✔
507
        return token
11✔
508
}
11✔
509

510
// getURLs creates URLs for the named experiment, running on the named machine
511
// for each given port. Every URL will include an `access_token=` parameter,
512
// authorizing the measurement.
513
func (c *Client) getURLs(ports static.Ports, hostname, token string, extra url.Values) map[string]string {
11✔
514
        urls := map[string]string{}
11✔
515
        // For each port config, prepare the target url with access_token and
11✔
516
        // complete host field.
11✔
517
        for _, target := range ports {
29✔
518
                name := target.String()
18✔
519
                params := url.Values{}
18✔
520
                params.Set("access_token", token)
18✔
521
                for key := range extra {
62✔
522
                        // note: we only use the first value.
44✔
523
                        params.Set(key, extra.Get(key))
44✔
524
                }
44✔
525
                target.RawQuery = params.Encode()
18✔
526

18✔
527
                host := &bytes.Buffer{}
18✔
528
                err := c.targetTmpl.Execute(host, map[string]string{
18✔
529
                        "Hostname": hostname,
18✔
530
                        "Ports":    target.Host, // from URL template, so typically just the ":port".
18✔
531
                })
18✔
532
                rtx.PanicOnError(err, "bad template evaluation")
18✔
533
                target.Host = host.String()
18✔
534
                urls[name] = target.String()
18✔
535
        }
536
        return urls
11✔
537
}
538

539
// limitRequest determines whether a client request should be rate-limited.
540
func (c *Client) limitRequest(now time.Time, req *http.Request) bool {
18✔
541
        agent := req.Header.Get("User-Agent")
18✔
542
        l, ok := c.agentLimits[agent]
18✔
543
        if !ok {
33✔
544
                // No limit defined for user agent.
15✔
545
                return false
15✔
546
        }
15✔
547
        return l.IsLimited(now)
3✔
548
}
549

550
// setHeaders sets the response headers for "nearest" requests.
551
func setHeaders(rw http.ResponseWriter) {
23✔
552
        // Set CORS policy to allow third-party websites to use returned resources.
23✔
553
        rw.Header().Set("Content-Type", "application/json")
23✔
554
        rw.Header().Set("Access-Control-Allow-Origin", "*")
23✔
555
        // Prevent caching of result.
23✔
556
        // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
23✔
557
        rw.Header().Set("Cache-Control", "no-store")
23✔
558
}
23✔
559

560
// writeResult marshals the result and writes the result to the response writer.
561
func writeResult(rw http.ResponseWriter, status int, result interface{}) {
27✔
562
        b, err := json.MarshalIndent(result, "", "  ")
27✔
563
        // Errors are only possible when marshalling incompatible types, like functions.
27✔
564
        rtx.PanicOnError(err, "Failed to format result")
27✔
565
        rw.WriteHeader(status)
27✔
566
        rw.Write(b)
27✔
567
}
27✔
568

569
// getExperimentAndService takes an http request path and extracts the last two
570
// fields. For correct requests (e.g. "/v2/nearest/ndt/ndt5"), this will be the
571
// experiment name (e.g. "ndt") and the datatype (e.g. "ndt5").
572
func getExperimentAndService(p string) (string, string) {
17✔
573
        datatype := path.Base(p)
17✔
574
        experiment := path.Base(path.Dir(p))
17✔
575
        return experiment, experiment + "/" + datatype
17✔
576
}
17✔
577

578
// getRemoteAddr extracts the remote address from the request. When running on
579
// Google App Engine, the X-Forwarded-For is guaranteed to be set. The GCP load
580
// balancer appends the actual client IP and the load balancer IP to any existing
581
// X-Forwarded-For header, so the second-to-last IP is the real client address.
582
// When running elsewhere (including on the local machine), the RemoteAddr from
583
// the request is used instead.
584
func getRemoteAddr(req *http.Request) string {
17✔
585
        xff := req.Header.Get("X-Forwarded-For")
17✔
586
        if xff != "" {
31✔
587
                // Split by comma and trim spaces from each IP
14✔
588
                ips := strings.Split(xff, ",")
14✔
589
                for i := range ips {
37✔
590
                        ips[i] = strings.TrimSpace(ips[i])
23✔
591
                }
23✔
592

593
                // GCP load balancer appends: <original-header>, <client-ip>, <lb-ip>
594
                // The second-to-last IP is the actual client address
595
                if len(ips) >= 2 {
18✔
596
                        return ips[len(ips)-2]
4✔
597
                } else if len(ips) == 1 && ips[0] != "" {
24✔
598
                        return ips[0]
10✔
599
                }
10✔
600
        }
601

602
        // Fall back to RemoteAddr for local testing or deployments outside of GAE.
603
        host, _, err := net.SplitHostPort(req.RemoteAddr)
3✔
604
        if err != nil {
4✔
605
                // As a last resort, use the whole RemoteAddr.
1✔
606
                return strings.TrimSpace(req.RemoteAddr)
1✔
607
        }
1✔
608
        return host
2✔
609
}
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