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

m-lab / locate / 24666372903

20 Apr 2026 12:21PM UTC coverage: 82.697% (+0.07%) from 82.629%
24666372903

Pull #260

github

robertodauria
refactor(handler): adapt to m-lab/access variadic Sign API
Pull Request #260: feat(handler): propagate int_id/key_id into priority access tokens

43 of 44 new or added lines in 3 files covered. (97.73%)

24 existing lines in 1 file now uncovered.

2514 of 3040 relevant lines covered (82.7%)

6.17 hits per line

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

88.94
/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. Extra claim objects are merged
43
// into the JWT payload via go-jose's variadic Claims support (see
44
// token.Signer.Sign).
45
type Signer interface {
46
        Sign(cl jwt.Claims, extra ...any) (string, error)
47
}
48

49
// IntegrationClaims holds M-Lab integration-specific claims that are embedded
50
// into access tokens issued for priority requests. The JSON tags are the
51
// wire-level claim names in the resulting JWT payload.
52
type IntegrationClaims struct {
53
        IntegrationID string `json:"int_id,omitempty"`
54
        KeyID         string `json:"key_id,omitempty"`
55
}
56

57
type Limiter interface {
58
        IsLimited(ip, ua string) (limits.LimitStatus, error)
59
}
60

61
// TierLimiter extends Limiter with tier-based limits for priority endpoints.
62
type TierLimiter interface {
63
        Limiter
64
        IsLimitedWithTier(org, ip string, tierConfig limits.LimitConfig) (limits.LimitStatus, error)
65
}
66

67
// Client handles HTTP requests for the locate service, including nearest server
68
// lookups, heartbeat processing, and monitoring endpoints. It maintains dependencies
69
// for geolocation, rate limiting, access token signing, and metrics collection.
70
// TODO: This should probably be a Handler, not a Client.
71
type Client struct {
72
        Signer
73
        project string
74
        LocatorV2
75
        ClientLocator
76
        PrometheusClient
77
        targetTmpl       *template.Template
78
        agentLimits      limits.Agents
79
        tierLimits       limits.TierLimits
80
        ipLimiter        TierLimiter
81
        earlyExitClients map[string]bool
82
        jwtVerifier      Verifier
83
}
84

85
// LocatorV2 defines how the Nearest handler requests machines nearest to the
86
// client.
87
type LocatorV2 interface {
88
        Nearest(service string, lat, lon float64, opts *heartbeat.NearestOptions) (*heartbeat.TargetInfo, error)
89
        heartbeat.StatusTracker
90
}
91

92
// ClientLocator defines the interface for looking up the client geolocation.
93
type ClientLocator interface {
94
        Locate(req *http.Request) (*clientgeo.Location, error)
95
}
96

97
// PrometheusClient defines the interface to query Prometheus.
98
type PrometheusClient interface {
99
        Query(ctx context.Context, query string, ts time.Time, opts ...prom.Option) (model.Value, prom.Warnings, error)
100
}
101

102
type paramOpts struct {
103
        raw       url.Values
104
        version   string
105
        ranks     map[string]int
106
        svcParams map[string]float64
107
}
108

109
func init() {
1✔
110
        log.SetFormatter(&log.JSONFormatter{})
1✔
111
        log.SetLevel(log.InfoLevel)
1✔
112
}
1✔
113

114
// NewClient creates a new client.
115
func NewClient(project string, private Signer, locatorV2 LocatorV2, client ClientLocator,
116
        promClient PrometheusClient, lmts limits.Agents, tierLmts limits.TierLimits, limiter TierLimiter, earlyExitClients []string, jwtVerifier Verifier) *Client {
47✔
117
        // Convert slice to map for O(1) lookups
47✔
118
        earlyExitMap := make(map[string]bool)
47✔
119
        for _, client := range earlyExitClients {
47✔
120
                earlyExitMap[client] = true
×
121
        }
×
122
        return &Client{
47✔
123
                Signer:           private,
47✔
124
                project:          project,
47✔
125
                LocatorV2:        locatorV2,
47✔
126
                ClientLocator:    client,
47✔
127
                PrometheusClient: promClient,
47✔
128
                targetTmpl:       template.Must(template.New("name").Parse("{{.Hostname}}{{.Ports}}")),
47✔
129
                agentLimits:      lmts,
47✔
130
                tierLimits:       tierLmts,
47✔
131
                ipLimiter:        limiter,
47✔
132
                earlyExitClients: earlyExitMap,
47✔
133
                jwtVerifier:      jwtVerifier,
47✔
134
        }
47✔
135
}
136

137
func (c *Client) extraParams(hostname string, index int, p paramOpts) url.Values {
21✔
138
        v := url.Values{}
21✔
139

21✔
140
        // Add client parameters.
21✔
141
        for key := range p.raw {
36✔
142
                if strings.HasPrefix(key, "client_") {
24✔
143
                        // note: we only use the first value.
9✔
144
                        v.Set(key, p.raw.Get(key))
9✔
145
                }
9✔
146

147
                val, ok := p.svcParams[key]
15✔
148
                if ok && rand.Float64() < val {
20✔
149
                        v.Set(key, p.raw.Get(key))
5✔
150
                }
5✔
151
        }
152

153
        // Add early_exit parameter for specified clients
154
        clientName := p.raw.Get("client_name")
21✔
155
        if clientName != "" && c.earlyExitClients[clientName] {
22✔
156
                v.Set(static.EarlyExitParameter, static.EarlyExitDefaultValue)
1✔
157
        }
1✔
158

159
        // Add Locate Service version.
160
        v.Set("locate_version", p.version)
21✔
161

21✔
162
        // Add metro rank.
21✔
163
        rank, ok := p.ranks[hostname]
21✔
164
        if ok {
25✔
165
                v.Set("metro_rank", strconv.Itoa(rank))
4✔
166
        }
4✔
167

168
        // Add result index.
169
        v.Set("index", strconv.Itoa(index))
21✔
170

21✔
171
        return v
21✔
172
}
173

174
// Nearest uses an implementation of the LocatorV2 interface to look up
175
// nearest servers.
176
func (c *Client) Nearest(rw http.ResponseWriter, req *http.Request) {
16✔
177
        result := v2.NearestResult{}
16✔
178
        setHeaders(rw)
16✔
179

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

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

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

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

228
        c.handleNearestRequest(rw, req, &result, "nearest", nil)
13✔
229
}
230

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

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

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

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

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

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

9✔
304
        // Handle CORS preflight requests. Use 200 instead of 204 for broader browser
9✔
305
        // compatibility (some Firefox versions reject 204 for CORS).
9✔
306
        //
9✔
307
        // See https://stackoverflow.com/a/46028619.
9✔
308
        if req.Method == http.MethodOptions {
10✔
309
                rw.WriteHeader(http.StatusOK)
1✔
310
                return
1✔
311
        }
1✔
312

313
        if err := req.ParseForm(); err != nil {
8✔
UNCOV
314
                log.Debugf("ParseForm error: %v", err)
×
UNCOV
315
                result.Error = v2.NewError("server", "Failed to parse form", http.StatusInternalServerError)
×
UNCOV
316
                writeResult(rw, result.Error.Status, result)
×
UNCOV
317
                metrics.RequestsTotal.WithLabelValues("priority_nearest", "server",
×
UNCOV
318
                        http.StatusText(result.Error.Status)).Inc()
×
319
                return
×
320
        }
×
321

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

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

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

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

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

1✔
383
                                clientName := req.Form.Get("client_name")
1✔
384
                                metrics.RateLimitedTotal.WithLabelValues(clientName, status.LimitType).Inc()
1✔
385

1✔
386
                                log.Debugf("Tier rate limit (%s) exceeded for int_id: %s, tier: %d, IP: %s, client: %s",
1✔
387
                                        status.LimitType, intID, tier, ip, clientName)
1✔
388
                                writeResult(rw, result.Error.Status, result)
1✔
389
                                return
1✔
390
                        }
1✔
UNCOV
391
                } else {
×
UNCOV
392
                        // This should never happen if Locate is deployed on AppEngine.
×
UNCOV
393
                        // For local deployments, RemoteAddr is assumed to always be there.
×
UNCOV
394
                        log.Warnf("Cannot find IP address for tier-based rate limiting.")
×
UNCOV
395
                }
×
396
        }
397

398
        // Extract key_id claim if present.
399
        keyID := ""
5✔
400
        if keyIDClaim, ok := claims["key_id"]; ok {
6✔
401
                if v, ok := keyIDClaim.(string); ok {
2✔
402
                        keyID = v
1✔
403
                }
1✔
404
        }
405

406
        // Rate limit passed - handle the nearest request
407
        ic := &IntegrationClaims{
5✔
408
                IntegrationID: intID,
5✔
409
                KeyID:         keyID,
5✔
410
        }
5✔
411
        c.handleNearestRequest(rw, req, result, "priority_nearest", ic)
5✔
412
}
413

414
// extractIntegrationID extracts the "int_id" claim from integration JWT claims.
415
func extractIntegrationID(claims map[string]interface{}) (string, error) {
7✔
416
        intIDClaim, ok := claims["int_id"]
7✔
417
        if !ok {
8✔
418
                return "", fmt.Errorf("int_id claim not found in JWT")
1✔
419
        }
1✔
420

421
        intID, ok := intIDClaim.(string)
6✔
422
        if !ok {
6✔
NEW
423
                return "", fmt.Errorf("int_id claim is not a string")
×
UNCOV
424
        }
×
425

426
        if intID == "" {
6✔
UNCOV
427
                return "", fmt.Errorf("int_id claim is empty")
×
UNCOV
428
        }
×
429

430
        return intID, nil
6✔
431
}
432

433
// Live is a minimal handler to indicate that the server is operating at all.
434
func (c *Client) Live(rw http.ResponseWriter, req *http.Request) {
2✔
435
        fmt.Fprintf(rw, "ok")
2✔
436
}
2✔
437

438
// Ready reports whether the server is working as expected and ready to serve requests.
439
func (c *Client) Ready(rw http.ResponseWriter, req *http.Request) {
2✔
440
        if c.LocatorV2.Ready() {
3✔
441
                fmt.Fprintf(rw, "ok")
1✔
442
        } else {
2✔
443
                rw.WriteHeader(http.StatusInternalServerError)
1✔
444
                fmt.Fprintf(rw, "not ready")
1✔
445
        }
1✔
446
}
447

448
// Registrations returns information about registered machines. There are 3
449
// supported query parameters:
450
//
451
// * format - defines the format of the returned JSON
452
// * org - limits results to only records for the given organization
453
// * exp - limits results to only records for the given experiment (e.g., ndt)
454
//
455
// The "org" and "exp" query parameters are currently only supported by the
456
// default or "machines" format.
457
func (c *Client) Registrations(rw http.ResponseWriter, req *http.Request) {
2✔
458
        var err error
2✔
459
        var result interface{}
2✔
460

2✔
461
        setHeaders(rw)
2✔
462

2✔
463
        q := req.URL.Query()
2✔
464
        format := q.Get("format")
2✔
465

2✔
466
        switch format {
2✔
UNCOV
467
        case "geo":
×
UNCOV
468
                result, err = siteinfo.Geo(c.LocatorV2.Instances(), q)
×
469
        default:
2✔
470
                result, err = siteinfo.Hosts(c.LocatorV2.Instances(), q)
2✔
471
        }
472

473
        if err != nil {
3✔
474
                v2Error := v2.NewError("siteinfo", err.Error(), http.StatusInternalServerError)
1✔
475
                writeResult(rw, http.StatusInternalServerError, v2Error)
1✔
476
                return
1✔
477
        }
1✔
478

479
        writeResult(rw, http.StatusOK, result)
1✔
480
}
481

482
// checkClientLocation looks up the client location and copies the location
483
// headers to the response writer.
484
func (c *Client) checkClientLocation(rw http.ResponseWriter, req *http.Request) (*clientgeo.Location, error) {
18✔
485
        // Lookup the client location using the client request.
18✔
486
        loc, err := c.Locate(req)
18✔
487
        if err != nil {
20✔
488
                return nil, errFailedToLookupClient
2✔
489
        }
2✔
490

491
        // Copy location headers to response writer.
492
        for key := range loc.Headers {
46✔
493
                rw.Header().Set(key, loc.Headers.Get(key))
30✔
494
        }
30✔
495

496
        return loc, nil
16✔
497
}
498

499
// populateURLs populates each set of URLs using the target configuration.
500
func (c *Client) populateURLs(targets []v2.Target, ports static.Ports, exp string, pOpts paramOpts, ic *IntegrationClaims) {
12✔
501
        for i, target := range targets {
24✔
502
                tkn := c.getAccessToken(target.Machine, exp, ic)
12✔
503
                params := c.extraParams(target.Machine, i, pOpts)
12✔
504
                targets[i].URLs = c.getURLs(ports, target.Hostname, tkn, params)
12✔
505
        }
12✔
506
}
507

508
// getAccessToken allocates a new access token using the given machine name as
509
// the intended audience and the subject as the target service.
510
func (c *Client) getAccessToken(machine, subject string, ic *IntegrationClaims) string {
13✔
511
        // Create the token. The same access token is reused for every URL of a
13✔
512
        // target port.
13✔
513
        // A uuid is added to the claims so that each new token is unique.
13✔
514
        cl := jwt.Claims{
13✔
515
                Issuer:   static.IssuerLocate,
13✔
516
                Subject:  subject,
13✔
517
                Audience: jwt.Audience{machine},
13✔
518
                Expiry:   jwt.NewNumericDate(time.Now().Add(time.Minute)),
13✔
519
                ID:       uuid.NewString(),
13✔
520
        }
13✔
521
        var t string
13✔
522
        var err error
13✔
523
        if ic != nil && ic.IntegrationID != "" {
18✔
524
                t, err = c.Sign(cl, *ic)
5✔
525
        } else {
13✔
526
                t, err = c.Sign(cl)
8✔
527
        }
8✔
528
        // Sign errors can only happen due to a misconfiguration of the key.
529
        // A good config will remain good.
530
        rtx.PanicOnError(err, "signing claims has failed")
13✔
531
        return t
13✔
532
}
533

534
// getURLs creates URLs for the named experiment, running on the named machine
535
// for each given port. Every URL will include an `access_token=` parameter,
536
// authorizing the measurement.
537
func (c *Client) getURLs(ports static.Ports, hostname, token string, extra url.Values) map[string]string {
13✔
538
        urls := map[string]string{}
13✔
539
        // For each port config, prepare the target url with access_token and
13✔
540
        // complete host field.
13✔
541
        for _, target := range ports {
33✔
542
                name := target.String()
20✔
543
                params := url.Values{}
20✔
544
                params.Set("access_token", token)
20✔
545
                for key := range extra {
68✔
546
                        // note: we only use the first value.
48✔
547
                        params.Set(key, extra.Get(key))
48✔
548
                }
48✔
549
                target.RawQuery = params.Encode()
20✔
550

20✔
551
                host := &bytes.Buffer{}
20✔
552
                err := c.targetTmpl.Execute(host, map[string]string{
20✔
553
                        "Hostname": hostname,
20✔
554
                        "Ports":    target.Host, // from URL template, so typically just the ":port".
20✔
555
                })
20✔
556
                rtx.PanicOnError(err, "bad template evaluation")
20✔
557
                target.Host = host.String()
20✔
558
                urls[name] = target.String()
20✔
559
        }
560
        return urls
13✔
561
}
562

563
// limitRequest determines whether a client request should be rate-limited.
564
func (c *Client) limitRequest(now time.Time, req *http.Request) bool {
20✔
565
        agent := req.Header.Get("User-Agent")
20✔
566
        l, ok := c.agentLimits[agent]
20✔
567
        if !ok {
37✔
568
                // No limit defined for user agent.
17✔
569
                return false
17✔
570
        }
17✔
571
        return l.IsLimited(now)
3✔
572
}
573

574
// setHeaders sets the response headers for "nearest" requests and
575
// other similar requests where we need:
576
//
577
// 1. Content-Type equal to application/json
578
//
579
// 2. Cache-Control equal to no-store
580
//
581
// 3. CORS headers
582
func setHeaders(rw http.ResponseWriter) {
30✔
583
        // Set the content-type header to imply we will return JSON
30✔
584
        rw.Header().Set("Content-Type", "application/json")
30✔
585

30✔
586
        // Set CORS policy to allow third-party websites to use returned resources.
30✔
587
        rw.Header().Set("Access-Control-Allow-Origin", "*")
30✔
588
        rw.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
30✔
589
        rw.Header().Set("Access-Control-Allow-Headers", "Authorization")
30✔
590

30✔
591
        // Prevent caching of result.
30✔
592
        // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
30✔
593
        rw.Header().Set("Cache-Control", "no-store")
30✔
594
}
30✔
595

596
// writeResult marshals the result and writes the result to the response writer.
597
func writeResult(rw http.ResponseWriter, status int, result interface{}) {
30✔
598
        b, err := json.MarshalIndent(result, "", "  ")
30✔
599
        // Errors are only possible when marshalling incompatible types, like functions.
30✔
600
        rtx.PanicOnError(err, "Failed to format result")
30✔
601
        rw.WriteHeader(status)
30✔
602
        rw.Write(b)
30✔
603
}
30✔
604

605
// getExperimentAndService takes an http request path and extracts the last two
606
// fields. For correct requests (e.g. "/v2/nearest/ndt/ndt5"), this will be the
607
// experiment name (e.g. "ndt") and the datatype (e.g. "ndt5").
608
func getExperimentAndService(p string) (string, string) {
20✔
609
        datatype := path.Base(p)
20✔
610
        experiment := path.Base(path.Dir(p))
20✔
611
        return experiment, experiment + "/" + datatype
20✔
612
}
20✔
613

614
// getRemoteAddr extracts the remote address from the request. When running on
615
// Google App Engine, the X-Forwarded-For is guaranteed to be set. The GCP load
616
// balancer appends the actual client IP and the load balancer IP to any existing
617
// X-Forwarded-For header, so the second-to-last IP is the real client address.
618
// When running elsewhere (including on the local machine), the RemoteAddr from
619
// the request is used instead.
620
func getRemoteAddr(req *http.Request) string {
18✔
621
        xff := req.Header.Get("X-Forwarded-For")
18✔
622
        if xff != "" {
33✔
623
                // Split by comma and trim spaces from each IP
15✔
624
                ips := strings.Split(xff, ",")
15✔
625
                for i := range ips {
39✔
626
                        ips[i] = strings.TrimSpace(ips[i])
24✔
627
                }
24✔
628

629
                // GCP load balancer appends: <original-header>, <client-ip>, <lb-ip>
630
                // The second-to-last IP is the actual client address
631
                if len(ips) >= 2 {
19✔
632
                        return ips[len(ips)-2]
4✔
633
                } else if len(ips) == 1 && ips[0] != "" {
26✔
634
                        return ips[0]
11✔
635
                }
11✔
636
        }
637

638
        // Fall back to RemoteAddr for local testing or deployments outside of GAE.
639
        host, _, err := net.SplitHostPort(req.RemoteAddr)
3✔
640
        if err != nil {
4✔
641
                // As a last resort, use the whole RemoteAddr.
1✔
642
                return strings.TrimSpace(req.RemoteAddr)
1✔
643
        }
1✔
644
        return host
2✔
645
}
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