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

m-lab / autojoin / 12059416068

27 Nov 2024 10:52PM UTC coverage: 98.142% (-0.8%) from 98.92%
12059416068

push

github

web-flow
Allow passing Type and Uplink in registration requests (#60)

* Allow receiving machine type and uplink speed in register reqs

Previously "Type" and "Uplink" in the v2.Registration struct were hardcoded as
"unknown". This change allows, and even requires, that the registration request
has nomimally valid values for those to fields.

* Adds real value for Type and Uplink to TestCreateRegisterResponse

* Removes debug line, and fixes an error message

36 of 36 new or added lines in 2 files covered. (100.0%)

10 existing lines in 1 file now uncovered.

1215 of 1238 relevant lines covered (98.14%)

1.08 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

585
func getPorts(req *http.Request) []string {
1✔
586
        result := []string{}
1✔
587
        ports := req.URL.Query()["ports"]
1✔
588
        for _, port := range ports {
2✔
589
                // Verify this is a valid number.
1✔
590
                _, err := strconv.ParseInt(port, 10, 64)
1✔
591
                if err != nil {
2✔
592
                        // Skip if not.
1✔
593
                        continue
1✔
594
                }
595
                result = append(result, port)
1✔
596
        }
597
        if len(result) == 0 {
2✔
598
                return []string{"9990"} // default port
1✔
599
        }
1✔
600
        return result
1✔
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

© 2025 Coveralls, Inc