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

m-lab / autojoin / 13413405967

18 Feb 2025 05:27PM UTC coverage: 94.374%. First build
13413405967

push

github

web-flow
Update autojoin API to verify keys from Datastore (#64)

* Use datastore to verify API keys

* Fix tests

* Debug

* Fix query to find key

* Add Key field to APIKey

* s/key/api_key/

* Update register command to not require org name anymore

* Add validation via datastore to the delete endpoint too

* Fix tests

53 of 61 new or added lines in 3 files covered. (86.89%)

1258 of 1333 relevant lines covered (94.37%)

1.04 hits per line

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

95.82
/handler/handler.go
1
package handler
2

3
import (
4
        "context"
5
        "encoding/json"
6
        "errors"
7
        "fmt"
8
        "log"
9
        "net"
10
        "net/http"
11
        "regexp"
12
        "strconv"
13
        "strings"
14

15
        v0 "github.com/m-lab/autojoin/api/v0"
16
        "github.com/m-lab/autojoin/iata"
17
        "github.com/m-lab/autojoin/internal/dnsname"
18
        "github.com/m-lab/autojoin/internal/dnsx"
19
        "github.com/m-lab/autojoin/internal/dnsx/dnsiface"
20
        "github.com/m-lab/autojoin/internal/register"
21
        "github.com/m-lab/gcp-service-discovery/discovery"
22
        "github.com/m-lab/go/host"
23
        "github.com/m-lab/go/rtx"
24
        v2 "github.com/m-lab/locate/api/v2"
25
        "github.com/m-lab/uuid-annotator/annotator"
26
        "github.com/oschwald/geoip2-golang"
27
)
28

29
var (
30
        errLocationNotFound = errors.New("location not found")
31
        errLocationFormat   = errors.New("location could not be parsed")
32

33
        validName = regexp.MustCompile(`[a-z0-9]+`)
34
)
35

36
// Server maintains shared state for the server.
37
type Server struct {
38
        Project string
39
        Iata    IataFinder
40
        Maxmind MaxmindFinder
41
        ASN     ASNFinder
42
        DNS     dnsiface.Service
43

44
        sm         ServiceAccountSecretManager
45
        dnsTracker DNSTracker
46
}
47

48
// ASNFinder is an interface used by the Server to manage ASN information.
49
type ASNFinder interface {
50
        AnnotateIP(src string) *annotator.Network
51
        Reload(ctx context.Context)
52
}
53

54
// MaxmindFinder is an interface used by the Server to manage Maxmind information.
55
type MaxmindFinder interface {
56
        City(ip net.IP) (*geoip2.City, error)
57
        Reload(ctx context.Context) error
58
}
59

60
// IataFinder is an interface used by the Server to manage IATA information.
61
type IataFinder interface {
62
        Lookup(country string, lat, lon float64) (string, error)
63
        Find(iata string) (iata.Row, error)
64
        Load(ctx context.Context) error
65
}
66

67
type DNSTracker interface {
68
        Update(string, []string) error
69
        Delete(string) error
70
        List() ([]string, [][]string, error)
71
}
72

73
// ServiceAccountSecretManager is an interface used by the server to allocate service account keys.
74
type ServiceAccountSecretManager interface {
75
        LoadOrCreateKey(ctx context.Context, org string) (string, error)
76
}
77

78
// NewServer creates a new Server instance for request handling.
79
func NewServer(project string, finder IataFinder, maxmind MaxmindFinder, asn ASNFinder,
80
        ds dnsiface.Service, tracker DNSTracker, sm ServiceAccountSecretManager) *Server {
1✔
81
        return &Server{
1✔
82
                Project: project,
1✔
83
                Iata:    finder,
1✔
84
                Maxmind: maxmind,
1✔
85
                ASN:     asn,
1✔
86
                DNS:     ds,
1✔
87
                sm:      sm,
1✔
88

1✔
89
                dnsTracker: tracker,
1✔
90
        }
1✔
91
}
1✔
92

93
// Reload reloads all resources used by the Server.
94
func (s *Server) Reload(ctx context.Context) {
1✔
95
        s.Iata.Load(ctx)
1✔
96
        s.Maxmind.Reload(ctx)
1✔
97
}
1✔
98

99
// Lookup is a handler used to find the nearest IATA given client IP or lat/lon metadata.
100
func (s *Server) Lookup(rw http.ResponseWriter, req *http.Request) {
1✔
101
        resp := v0.LookupResponse{}
1✔
102
        country, err := s.getCountry(req)
1✔
103
        if country == "" || err != nil {
2✔
104
                resp.Error = &v2.Error{
1✔
105
                        Type:   "?country=<country>",
1✔
106
                        Title:  "could not determine country from request",
1✔
107
                        Status: http.StatusBadRequest,
1✔
108
                }
1✔
109
                rw.WriteHeader(resp.Error.Status)
1✔
110
                writeResponse(rw, resp)
1✔
111
                return
1✔
112
        }
1✔
113
        lat, lon, err := s.getLocation(req)
1✔
114
        if err != nil {
2✔
115
                resp.Error = &v2.Error{
1✔
116
                        Type:   "?lat=<lat>&lon=<lon>",
1✔
117
                        Title:  "could not determine lat/lon from request",
1✔
118
                        Status: http.StatusBadRequest,
1✔
119
                }
1✔
120
                rw.WriteHeader(resp.Error.Status)
1✔
121
                writeResponse(rw, resp)
1✔
122
                return
1✔
123
        }
1✔
124
        code, err := s.Iata.Lookup(country, lat, lon)
1✔
125
        if err != nil {
2✔
126
                resp.Error = &v2.Error{
1✔
127
                        Type:   "internal error",
1✔
128
                        Title:  "could not determine iata from request",
1✔
129
                        Status: http.StatusInternalServerError,
1✔
130
                }
1✔
131
                rw.WriteHeader(resp.Error.Status)
1✔
132
                writeResponse(rw, resp)
1✔
133
                return
1✔
134
        }
1✔
135
        resp.Lookup = &v0.Lookup{
1✔
136
                IATA: code,
1✔
137
        }
1✔
138
        writeResponse(rw, resp)
1✔
139
}
140

141
// Register handler is used by autonodes to register their hostname with M-Lab
142
// on startup and receive additional needed configuration metadata.
143
func (s *Server) Register(rw http.ResponseWriter, req *http.Request) {
1✔
144
        // All replies, errors and successes, should be json.
1✔
145
        rw.Header().Set("Content-Type", "application/json")
1✔
146

1✔
147
        resp := v0.RegisterResponse{}
1✔
148
        param := &register.Params{Project: s.Project}
1✔
149
        param.Service = req.URL.Query().Get("service")
1✔
150
        if !isValidName(param.Service) {
2✔
151
                resp.Error = &v2.Error{
1✔
152
                        Type:   "?service=<service>",
1✔
153
                        Title:  "could not determine service from request",
1✔
154
                        Status: http.StatusBadRequest,
1✔
155
                }
1✔
156
                rw.WriteHeader(resp.Error.Status)
1✔
157
                writeResponse(rw, resp)
1✔
158
                return
1✔
159
        }
1✔
160

161
        // Get the organization from the context.
162
        org, ok := req.Context().Value(orgContextKey).(string)
1✔
163
        if !ok {
1✔
164
                resp.Error = &v2.Error{
×
NEW
165
                        Type:   "auth.context",
×
NEW
166
                        Title:  "missing organization in context",
×
NEW
167
                        Status: http.StatusInternalServerError,
×
168
                }
×
169
                rw.WriteHeader(resp.Error.Status)
×
170
                writeResponse(rw, resp)
×
171
                return
×
172
        }
×
173
        param.Org = org
1✔
174
        param.IPv6 = checkIP(req.URL.Query().Get("ipv6")) // optional.
1✔
175
        param.IPv4 = checkIP(getClientIP(req))
1✔
176
        ip := net.ParseIP(param.IPv4)
1✔
177
        if ip == nil || ip.To4() == nil {
2✔
178
                resp.Error = &v2.Error{
1✔
179
                        Type:   "?ipv4=<ipv4>",
1✔
180
                        Title:  "could not determine client ipv4 from request",
1✔
181
                        Status: http.StatusBadRequest,
1✔
182
                }
1✔
183
                rw.WriteHeader(resp.Error.Status)
1✔
184
                writeResponse(rw, resp)
1✔
185
                return
1✔
186
        }
1✔
187
        param.Type = req.URL.Query().Get("type")
1✔
188
        if !isValidType(param.Type) {
2✔
189
                resp.Error = &v2.Error{
1✔
190
                        Type:   "?type=<type>",
1✔
191
                        Title:  "invalid machine type from request",
1✔
192
                        Status: http.StatusBadRequest,
1✔
193
                }
1✔
194
                rw.WriteHeader(resp.Error.Status)
1✔
195
                writeResponse(rw, resp)
1✔
196
                return
1✔
197
        }
1✔
198
        param.Uplink = req.URL.Query().Get("uplink")
1✔
199
        if !isValidUplink(param.Uplink) {
2✔
200
                resp.Error = &v2.Error{
1✔
201
                        Type:   "?uplink=<uplink>",
1✔
202
                        Title:  "invalid uplink speed from request",
1✔
203
                        Status: http.StatusBadRequest,
1✔
204
                }
1✔
205
                rw.WriteHeader(resp.Error.Status)
1✔
206
                writeResponse(rw, resp)
1✔
207
                return
1✔
208
        }
1✔
209
        iata := getClientIata(req)
1✔
210
        if iata == "" {
1✔
211
                resp.Error = &v2.Error{
×
212
                        Type:   "?iata=<iata>",
×
213
                        Title:  "could not determine iata from request",
×
214
                        Status: http.StatusBadRequest,
×
215
                }
×
216
                rw.WriteHeader(resp.Error.Status)
×
217
                writeResponse(rw, resp)
×
218
                return
×
219
        }
×
220
        row, err := s.Iata.Find(iata)
1✔
221
        if err != nil {
2✔
222
                resp.Error = &v2.Error{
1✔
223
                        Type:   "iata.find",
1✔
224
                        Title:  "could not find given iata in dataset",
1✔
225
                        Status: http.StatusInternalServerError,
1✔
226
                }
1✔
227
                rw.WriteHeader(resp.Error.Status)
1✔
228
                writeResponse(rw, resp)
1✔
229
                return
1✔
230
        }
1✔
231
        param.Metro = row
1✔
232
        record, err := s.Maxmind.City(ip)
1✔
233
        if err != nil {
2✔
234
                resp.Error = &v2.Error{
1✔
235
                        Type:   "maxmind.city",
1✔
236
                        Title:  "could not find city metadata from ip",
1✔
237
                        Status: http.StatusInternalServerError,
1✔
238
                }
1✔
239
                rw.WriteHeader(resp.Error.Status)
1✔
240
                writeResponse(rw, resp)
1✔
241
                return
1✔
242
        }
1✔
243
        param.Geo = record
1✔
244
        param.Network = s.ASN.AnnotateIP(param.IPv4)
1✔
245
        // Override site probability with user-provided parameter.
1✔
246
        // TODO(soltesz): include M-Lab override option
1✔
247
        param.Probability = getProbability(req)
1✔
248
        r := register.CreateRegisterResponse(param)
1✔
249

1✔
250
        key, err := s.sm.LoadOrCreateKey(req.Context(), param.Org)
1✔
251
        if err != nil {
2✔
252
                resp.Error = &v2.Error{
1✔
253
                        Type:   "load.serviceaccount.key",
1✔
254
                        Title:  "could not load service account key for node",
1✔
255
                        Status: http.StatusInternalServerError,
1✔
256
                }
1✔
257
                log.Println("loading service account key failure:", err)
1✔
258
                rw.WriteHeader(resp.Error.Status)
1✔
259
                writeResponse(rw, resp)
1✔
260
                return
1✔
261
        }
1✔
262
        r.Registration.Credentials = &v0.Credentials{
1✔
263
                ServiceAccountKey: key,
1✔
264
        }
1✔
265

1✔
266
        // Register the hostname under the organization zone.
1✔
267
        m := dnsx.NewManager(s.DNS, s.Project, dnsname.OrgZone(param.Org, s.Project))
1✔
268
        _, err = m.Register(req.Context(), r.Registration.Hostname+".", param.IPv4, param.IPv6)
1✔
269
        if err != nil {
2✔
270
                resp.Error = &v2.Error{
1✔
271
                        Type:   "dns.register",
1✔
272
                        Title:  "could not register dynamic hostname",
1✔
273
                        Status: http.StatusInternalServerError,
1✔
274
                }
1✔
275
                log.Println("dns register failure:", err)
1✔
276
                rw.WriteHeader(resp.Error.Status)
1✔
277
                writeResponse(rw, resp)
1✔
278
                return
1✔
279
        }
1✔
280

281
        // Add the hostname to the DNS tracker.
282
        err = s.dnsTracker.Update(r.Registration.Hostname, getPorts(req))
1✔
283
        if err != nil {
2✔
284
                resp.Error = &v2.Error{
1✔
285
                        Type:   "tracker.gc",
1✔
286
                        Title:  "could not update DNS tracker",
1✔
287
                        Status: http.StatusInternalServerError,
1✔
288
                }
1✔
289
                log.Println("dns gc update failure:", err)
1✔
290
                rw.WriteHeader(resp.Error.Status)
1✔
291
                writeResponse(rw, resp)
1✔
292
                return
1✔
293
        }
1✔
294

295
        b, _ := json.MarshalIndent(r, "", " ")
1✔
296
        rw.Write(b)
1✔
297
}
298

299
// Delete handler is used by operators to delete a previously registered
300
// hostname from DNS.
301
func (s *Server) Delete(rw http.ResponseWriter, req *http.Request) {
1✔
302
        // All replies, errors and successes, should be json.
1✔
303
        rw.Header().Set("Content-Type", "application/json")
1✔
304

1✔
305
        resp := v0.DeleteResponse{}
1✔
306
        hostname := req.URL.Query().Get("hostname")
1✔
307
        name, err := host.Parse(hostname)
1✔
308
        if err != nil {
2✔
309
                resp.Error = &v2.Error{
1✔
310
                        Type:   "dns.delete",
1✔
311
                        Title:  "failed to parse hostname",
1✔
312
                        Detail: err.Error(),
1✔
313
                        Status: http.StatusBadRequest,
1✔
314
                }
1✔
315
                log.Println("dns delete (parse) failure:", err)
1✔
316
                rw.WriteHeader(resp.Error.Status)
1✔
317
                writeResponse(rw, resp)
1✔
318
                return
1✔
319
        }
1✔
320

321
        m := dnsx.NewManager(s.DNS, s.Project, dnsname.OrgZone(name.Org, s.Project))
1✔
322
        _, err = m.Delete(req.Context(), name.StringAll()+".")
1✔
323
        if err != nil {
2✔
324
                resp.Error = &v2.Error{
1✔
325
                        Type:   "dns.delete",
1✔
326
                        Title:  "failed to delete hostname",
1✔
327
                        Detail: err.Error(),
1✔
328
                        Status: http.StatusInternalServerError,
1✔
329
                }
1✔
330
                log.Println("dns delete failure:", err)
1✔
331
                rw.WriteHeader(resp.Error.Status)
1✔
332
                writeResponse(rw, resp)
1✔
333
                return
1✔
334
        }
1✔
335

336
        err = s.dnsTracker.Delete(name.StringAll())
1✔
337
        if err != nil {
2✔
338
                resp.Error = &v2.Error{
1✔
339
                        Type:   "tracker.gc",
1✔
340
                        Title:  "failed to delete hostname from DNS tracker",
1✔
341
                        Detail: err.Error(),
1✔
342
                        Status: http.StatusInternalServerError,
1✔
343
                }
1✔
344
                log.Println("dns gc delete failure:", err)
1✔
345
                rw.WriteHeader(resp.Error.Status)
1✔
346
                writeResponse(rw, resp)
1✔
347
                return
1✔
348
        }
1✔
349

350
        b, err := json.MarshalIndent(resp, "", " ")
1✔
351
        rtx.Must(err, "failed to marshal DNS delete response")
1✔
352
        rw.Write(b)
1✔
353
}
354

355
// List handler is used by monitoring to generate a list of known, active
356
// hostnames previously registered with the Autojoin API.
357
func (s *Server) List(rw http.ResponseWriter, req *http.Request) {
1✔
358
        // Set CORS policy to allow third-party websites to use returned resources.
1✔
359
        rw.Header().Set("Content-Type", "application/json")
1✔
360
        rw.Header().Set("Access-Control-Allow-Origin", "*")
1✔
361
        rw.Header().Set("Cache-Control", "no-store") // Prevent caching of result.
1✔
362

1✔
363
        configs := []discovery.StaticConfig{}
1✔
364
        resp := v0.ListResponse{}
1✔
365
        hosts, ports, err := s.dnsTracker.List()
1✔
366
        if err != nil {
2✔
367
                resp.Error = &v2.Error{
1✔
368
                        Type:   "list",
1✔
369
                        Title:  "failed to list node records",
1✔
370
                        Detail: err.Error(),
1✔
371
                        Status: http.StatusInternalServerError,
1✔
372
                }
1✔
373
                log.Println("list failure:", err)
1✔
374
                rw.WriteHeader(resp.Error.Status)
1✔
375
                writeResponse(rw, resp)
1✔
376
                return
1✔
377
        }
1✔
378

379
        org := req.URL.Query().Get("org")
1✔
380
        format := req.URL.Query().Get("format")
1✔
381
        sites := map[string]bool{}
1✔
382

1✔
383
        // Create a prometheus StaticConfig for each known host.
1✔
384
        for i := range hosts {
2✔
385
                h, err := host.Parse(hosts[i])
1✔
386
                if err != nil {
2✔
387
                        continue
1✔
388
                }
389
                if org != "" && org != h.Org {
2✔
390
                        // Skip hosts that are not part of the given org.
1✔
391
                        continue
1✔
392
                }
393
                sites[h.Site] = true
1✔
394
                if format == "script-exporter" {
2✔
395
                        // NOTE: do not assign any ports for script exporter.
1✔
396
                        ports[i] = []string{""}
1✔
397
                } else {
2✔
398
                        // Convert port strings to ":<port>".
1✔
399
                        p := []string{}
1✔
400
                        for j := range ports[i] {
2✔
401
                                p = append(p, ":"+ports[i][j])
1✔
402
                        }
1✔
403
                        ports[i] = p
1✔
404
                }
405
                for _, port := range ports[i] {
2✔
406
                        labels := map[string]string{
1✔
407
                                "machine":    hosts[i],
1✔
408
                                "type":       "virtual",
1✔
409
                                "deployment": "byos",
1✔
410
                                "managed":    "none",
1✔
411
                                "org":        h.Org,
1✔
412
                        }
1✔
413
                        if req.URL.Query().Get("service") != "" {
2✔
414
                                labels["service"] = req.URL.Query().Get("service")
1✔
415
                        }
1✔
416
                        // We create one record per host to add a unique "machine" label to each one.
417
                        configs = append(configs, discovery.StaticConfig{
1✔
418
                                Targets: []string{hosts[i] + port},
1✔
419
                                Labels:  labels,
1✔
420
                        })
1✔
421
                }
422
        }
423

424
        var results interface{}
1✔
425
        switch format {
1✔
426
        case "script-exporter":
1✔
427
                fallthrough
1✔
428
        case "blackbox":
1✔
429
                fallthrough
1✔
430
        case "prometheus":
1✔
431
                results = configs
1✔
432
        case "servers":
1✔
433
                resp.Servers = hosts
1✔
434
                results = resp
1✔
435
        case "sites":
1✔
436
                for k := range sites {
2✔
437
                        resp.Sites = append(resp.Sites, k)
1✔
438
                }
1✔
439
                results = resp
1✔
440
        default:
1✔
441
                resp.Servers = hosts
1✔
442
                results = resp
1✔
443
        }
444
        // Generate as JSON; the list may be empty.
445
        b, err := json.MarshalIndent(results, "", " ")
1✔
446
        rtx.Must(err, "failed to marshal DNS delete response")
1✔
447
        rw.Write(b)
1✔
448
}
449

450
// Live reports whether the system is live.
451
func (s *Server) Live(rw http.ResponseWriter, req *http.Request) {
1✔
452
        fmt.Fprintf(rw, "ok")
1✔
453
}
1✔
454

455
// Ready reports whether the server is ready.
456
func (s *Server) Ready(rw http.ResponseWriter, req *http.Request) {
1✔
457
        fmt.Fprintf(rw, "ok")
1✔
458
}
1✔
459

460
func getClientIata(req *http.Request) string {
1✔
461
        iata := req.URL.Query().Get("iata")
1✔
462
        if iata != "" && len(iata) == 3 && isValidName(iata) {
2✔
463
                return strings.ToLower(iata)
1✔
464
        }
1✔
465
        return ""
×
466
}
467

468
func isValidName(s string) bool {
1✔
469
        if s == "" {
2✔
470
                return false
1✔
471
        }
1✔
472
        if len(s) > 10 {
2✔
473
                return false
1✔
474
        }
1✔
475
        return validName.MatchString(s)
1✔
476
}
477

478
func isValidType(s string) bool {
1✔
479
        switch s {
1✔
480
        case "physical", "virtual":
1✔
481
                return true
1✔
482
        default:
1✔
483
                return false
1✔
484
        }
485
}
486

487
func isValidUplink(s string) bool {
1✔
488
        // Minimally make sure the uplink speed specification looks like some
1✔
489
        // numbers followed by "g".
1✔
490
        matched, _ := regexp.MatchString("[0-9]+g", s)
1✔
491
        return matched
1✔
492
}
1✔
493

494
func (s *Server) getCountry(req *http.Request) (string, error) {
1✔
495
        c := req.URL.Query().Get("country")
1✔
496
        if c != "" {
2✔
497
                return c, nil
1✔
498
        }
1✔
499
        c = req.Header.Get("X-AppEngine-Country")
1✔
500
        if c != "" {
2✔
501
                return c, nil
1✔
502
        }
1✔
503
        record, err := s.Maxmind.City(net.ParseIP(getClientIP(req)))
1✔
504
        if err != nil {
2✔
505
                return "", err
1✔
506
        }
1✔
507
        return record.Country.IsoCode, nil
1✔
508
}
509

510
func rawLatLon(req *http.Request) (string, string, error) {
1✔
511
        lat := req.URL.Query().Get("lat")
1✔
512
        lon := req.URL.Query().Get("lon")
1✔
513
        if lat != "" && lon != "" {
2✔
514
                return lat, lon, nil
1✔
515
        }
1✔
516
        latlon := req.Header.Get("X-AppEngine-CityLatLong")
1✔
517
        if latlon != "0.000000,0.000000" {
2✔
518
                fields := strings.Split(latlon, ",")
1✔
519
                if len(fields) == 2 {
2✔
520
                        return fields[0], fields[1], nil
1✔
521
                }
1✔
522
        }
523
        return "", "", errLocationNotFound
1✔
524
}
525

526
func (s *Server) getLocation(req *http.Request) (float64, float64, error) {
1✔
527
        rlat, rlon, err := rawLatLon(req)
1✔
528
        if err == nil {
2✔
529
                lat, errLat := strconv.ParseFloat(rlat, 64)
1✔
530
                lon, errLon := strconv.ParseFloat(rlon, 64)
1✔
531
                if errLat != nil || errLon != nil {
2✔
532
                        return 0, 0, errLocationFormat
1✔
533
                }
1✔
534
                return lat, lon, nil
1✔
535
        }
536
        // Fall back to lookup with request IP.
537
        record, err := s.Maxmind.City(net.ParseIP(getClientIP(req)))
1✔
538
        if err != nil {
2✔
539
                return 0, 0, err
1✔
540
        }
1✔
541
        return record.Location.Latitude, record.Location.Longitude, nil
1✔
542
}
543

544
func writeResponse(rw http.ResponseWriter, resp interface{}) {
1✔
545
        b, err := json.MarshalIndent(resp, "", "  ")
1✔
546
        // NOTE: marshal can only fail on incompatible types, like functions. The
1✔
547
        // panic will be caught by the http server handler.
1✔
548
        rtx.PanicOnError(err, "failed to marshal response")
1✔
549
        rw.Write(b)
1✔
550
}
1✔
551

552
func checkIP(ip string) string {
1✔
553
        if net.ParseIP(ip) != nil {
2✔
554
                return ip
1✔
555
        }
1✔
556
        return ""
1✔
557
}
558

559
func getClientIP(req *http.Request) string {
1✔
560
        // Use given IP parameter.
1✔
561
        rawip := req.URL.Query().Get("ipv4")
1✔
562
        if rawip != "" {
2✔
563
                return rawip
1✔
564
        }
1✔
565
        // Use AppEngine's forwarded client address.
566
        fwdIPs := strings.Split(req.Header.Get("X-Forwarded-For"), ", ")
1✔
567
        if fwdIPs[0] != "" {
2✔
568
                return fwdIPs[0]
1✔
569
        }
1✔
570
        // Use remote client address.
571
        hip, _, _ := net.SplitHostPort(req.RemoteAddr)
1✔
572
        return hip
1✔
573
}
574

575
func getProbability(req *http.Request) float64 {
1✔
576
        prob := req.URL.Query().Get("probability")
1✔
577
        if prob == "" {
2✔
578
                return 1.0
1✔
579
        }
1✔
580
        p, err := strconv.ParseFloat(prob, 64)
1✔
581
        if err != nil {
2✔
582
                return 1.0
1✔
583
        }
1✔
584
        return p
1✔
585
}
586

587
func getPorts(req *http.Request) []string {
1✔
588
        result := []string{}
1✔
589
        ports := req.URL.Query()["ports"]
1✔
590
        for _, port := range ports {
2✔
591
                // Verify this is a valid number.
1✔
592
                _, err := strconv.ParseInt(port, 10, 64)
1✔
593
                if err != nil {
2✔
594
                        // Skip if not.
1✔
595
                        continue
1✔
596
                }
597
                result = append(result, port)
1✔
598
        }
599
        if len(result) == 0 {
2✔
600
                return []string{"9990"} // default port
1✔
601
        }
1✔
602
        return result
1✔
603
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc