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

m-lab / locate / 21041393438

15 Jan 2026 06:07PM UTC coverage: 82.522% (-0.3%) from 82.781%
21041393438

Pull #238

github

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

148 of 189 new or added lines in 4 files covered. (78.31%)

10 existing lines in 1 file now uncovered.

2389 of 2895 relevant lines covered (82.52%)

6.04 hits per line

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

90.43
/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✔
UNCOV
218
                } else {
×
UNCOV
219
                        // This should never happen if Locate is deployed on AppEngine.
×
UNCOV
220
                        log.Println("Cannot find IP address for rate limiting.")
×
UNCOV
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✔
UNCOV
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✔
UNCOV
268
                country = q.Get("country")
×
UNCOV
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
        result := &v2.NearestResult{}
7✔
298
        setHeaders(rw)
7✔
299

7✔
300
        if err := req.ParseForm(); err != nil {
7✔
NEW
301
                log.Debugf("ParseForm error: %v", err)
×
NEW
302
                result.Error = v2.NewError("server", "Failed to parse form", http.StatusInternalServerError)
×
NEW
303
                writeResult(rw, result.Error.Status, result)
×
NEW
304
                metrics.RequestsTotal.WithLabelValues("priority_nearest", "server",
×
NEW
305
                        http.StatusText(result.Error.Status)).Inc()
×
NEW
306
                return
×
NEW
307
        }
×
308

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

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

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

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

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

1✔
370
                                clientName := req.Form.Get("client_name")
1✔
371
                                metrics.RateLimitedTotal.WithLabelValues(clientName, status.LimitType).Inc()
1✔
372

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

384
        // Rate limit passed - handle the nearest request
385
        c.handleNearestRequest(rw, req, result, "priority_nearest")
4✔
386
}
387

388
// extractIntegrationID extracts the "int_id" claim from integration JWT claims.
389
func extractIntegrationID(claims map[string]interface{}) (string, error) {
6✔
390
        intIDClaim, ok := claims["int_id"]
6✔
391
        if !ok {
7✔
392
                return "", fmt.Errorf("int_id claim not found in JWT")
1✔
393
        }
1✔
394

395
        intID, ok := intIDClaim.(string)
5✔
396
        if !ok {
5✔
NEW
397
                return "", fmt.Errorf("int_id claim is not a string")
×
NEW
398
        }
×
399

400
        if intID == "" {
5✔
NEW
401
                return "", fmt.Errorf("int_id claim is empty")
×
NEW
402
        }
×
403

404
        return intID, nil
5✔
405
}
406

407
// Live is a minimal handler to indicate that the server is operating at all.
408
func (c *Client) Live(rw http.ResponseWriter, req *http.Request) {
2✔
409
        fmt.Fprintf(rw, "ok")
2✔
410
}
2✔
411

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

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

2✔
435
        setHeaders(rw)
2✔
436

2✔
437
        q := req.URL.Query()
2✔
438
        format := q.Get("format")
2✔
439

2✔
440
        switch format {
2✔
UNCOV
441
        case "geo":
×
UNCOV
442
                result, err = siteinfo.Geo(c.LocatorV2.Instances(), q)
×
443
        default:
2✔
444
                result, err = siteinfo.Hosts(c.LocatorV2.Instances(), q)
2✔
445
        }
446

447
        if err != nil {
3✔
448
                v2Error := v2.NewError("siteinfo", err.Error(), http.StatusInternalServerError)
1✔
449
                writeResult(rw, http.StatusInternalServerError, v2Error)
1✔
450
                return
1✔
451
        }
1✔
452

453
        writeResult(rw, http.StatusOK, result)
1✔
454
}
455

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

465
        // Copy location headers to response writer.
466
        for key := range loc.Headers {
37✔
467
                rw.Header().Set(key, loc.Headers.Get(key))
24✔
468
        }
24✔
469

470
        return loc, nil
13✔
471
}
472

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

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

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

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

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

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

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

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

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

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

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