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

umputun / reproxy / 20124601431

11 Dec 2025 06:51AM UTC coverage: 78.502% (+0.6%) from 77.945%
20124601431

Pull #237

github

umputun
fix(auth): address code review findings

- add tests for passwords containing colons
- add test for empty password edge case
- clarify test name for username with colon rejection
- fix comment precision in validateBasicAuthCredentials
- make ParseOnlyFrom filter empty entries like ParseAuth
Pull Request #237: feat(auth): add per-route basic authentication

70 of 72 new or added lines in 6 files covered. (97.22%)

40 existing lines in 2 files now uncovered.

2359 of 3005 relevant lines covered (78.5%)

21.17 hits per line

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

78.73
/app/proxy/proxy.go
1
package proxy
2

3
import (
4
        "bytes"
5
        "context"
6
        "crypto/tls"
7
        "fmt"
8
        "io"
9
        "net"
10
        "net/http"
11
        "net/http/httputil"
12
        "net/url"
13
        "os"
14
        "path/filepath"
15
        "regexp"
16
        "strconv"
17
        "strings"
18
        "time"
19

20
        log "github.com/go-pkgz/lgr"
21
        R "github.com/go-pkgz/rest"
22
        "github.com/go-pkgz/rest/logger"
23

24
        "github.com/umputun/reproxy/app/discovery"
25
        "github.com/umputun/reproxy/app/plugin"
26
)
27

28
// Http is a proxy server for both http and https
29
type Http struct { // nolint golint
30
        Matcher
31
        Address          string
32
        AssetsLocation   string
33
        AssetsWebRoot    string
34
        Assets404        string
35
        AssetsSPA        bool
36
        MaxBodySize      int64
37
        GzEnabled        bool
38
        ProxyHeaders     []string
39
        DropHeader       []string
40
        SSLConfig        SSLConfig
41
        Insecure         bool
42
        Version          string
43
        AccessLog        io.Writer
44
        StdOutEnabled    bool
45
        Signature        bool
46
        Timeouts         Timeouts
47
        CacheControl     MiddlewareProvider
48
        Metrics          MiddlewareProvider
49
        PluginConductor  MiddlewareProvider
50
        Reporter         Reporter
51
        LBSelector       LBSelector
52
        OnlyFrom         *OnlyFrom
53
        PerRouteAuth     *PerRouteAuth
54
        BasicAuthEnabled bool
55
        BasicAuthAllowed []string
56

57
        ThrottleSystem int
58
        ThrottleUser   int
59

60
        KeepHost bool
61

62
        UpstreamMaxIdleConns    int
63
        UpstreamMaxConnsPerHost int
64

65
        dnsResolvers []string // used to mock DNS resolvers for testing
66
}
67

68
// Matcher source info (server and route) to the destination url
69
// If no match found return ok=false
70
type Matcher interface {
71
        Match(srv, src string) (res discovery.Matches)
72
        Servers() (servers []string)
73
        Mappers() (mappers []discovery.URLMapper)
74
        CheckHealth() (pingResult map[string]error)
75
}
76

77
// MiddlewareProvider interface defines http middleware handler
78
type MiddlewareProvider interface {
79
        Middleware(next http.Handler) http.Handler
80
}
81

82
// Reporter defines error reporting service
83
type Reporter interface {
84
        Report(w http.ResponseWriter, code int)
85
}
86

87
// LBSelector defines load balancer strategy
88
type LBSelector interface {
89
        Select(size int) int // return index of picked server
90
}
91

92
// Timeouts consolidate timeouts for both server and transport
93
type Timeouts struct {
94
        // server timeouts
95
        ReadHeader time.Duration
96
        Write      time.Duration
97
        Idle       time.Duration
98
        // transport timeouts
99
        Dial           time.Duration
100
        KeepAlive      time.Duration
101
        IdleConn       time.Duration
102
        TLSHandshake   time.Duration
103
        ExpectContinue time.Duration
104
        ResponseHeader time.Duration
105
}
106

107
// Run the lister and request's router, activate rest server
108
func (h *Http) Run(ctx context.Context) error {
17✔
109

17✔
110
        if h.AssetsLocation != "" {
20✔
111
                log.Printf("[DEBUG] assets file server enabled for %s, webroot %s", h.AssetsLocation, h.AssetsWebRoot)
3✔
112
                if h.Assets404 != "" {
4✔
113
                        log.Printf("[DEBUG] assets 404 file enabled for %s", h.Assets404)
1✔
114
                }
1✔
115
        }
116

117
        if h.LBSelector == nil {
34✔
118
                h.LBSelector = &RandomSelector{}
17✔
119
        }
17✔
120

121
        if h.PerRouteAuth == nil {
33✔
122
                h.PerRouteAuth = NewPerRouteAuth()
16✔
123
        }
16✔
124

125
        var httpServer, httpsServer *http.Server
17✔
126

17✔
127
        go func() {
34✔
128
                <-ctx.Done()
17✔
129
                if httpServer != nil {
34✔
130
                        if err := httpServer.Close(); err != nil {
17✔
131
                                log.Printf("[ERROR] failed to close proxy http server, %v", err)
×
132
                        }
×
133
                }
134
                if httpsServer != nil {
18✔
135
                        if err := httpsServer.Close(); err != nil {
1✔
UNCOV
136
                                log.Printf("[ERROR] failed to close proxy https server, %v", err)
×
NEW
137
                        }
×
138
                }
139
        }()
140

141
        handler := R.Wrap(h.proxyHandler(),
17✔
142
                R.Recoverer(log.Default()),                   // recover on errors
17✔
143
                signatureHandler(h.Signature, h.Version),     // send app signature
17✔
144
                h.pingHandler,                                // respond to /ping
17✔
145
                h.healthMiddleware,                           // respond to /health
17✔
146
                h.matchHandler,                               // set matched routes to context
17✔
147
                h.OnlyFrom.Handler,                           // limit source (remote) IPs if defined
17✔
148
                h.PerRouteAuth.Handler,                       // per-route basic auth (if route has auth configured)
17✔
149
                h.basicAuthHandler(),                         // global basic auth (skipped if per-route auth is set)
17✔
150
                limiterSystemHandler(h.ThrottleSystem),       // limit total requests/sec
17✔
151
                limiterUserHandler(h.ThrottleUser),           // req/seq per user/route match
17✔
152
                h.mgmtHandler(),                              // handles /metrics and /routes for prometheus
17✔
153
                h.pluginHandler(),                            // prc to external plugins
17✔
154
                headersHandler(h.ProxyHeaders, h.DropHeader), // add response headers and delete some request headers
17✔
155
                accessLogHandler(h.AccessLog),                // apache-format log file
17✔
156
                stdoutLogHandler(h.StdOutEnabled, logger.New(logger.Log(log.Default()), logger.Prefix("[INFO]")).Handler),
17✔
157
                maxReqSizeHandler(h.MaxBodySize), // limit request max size
17✔
158
                gzipHandler(h.GzEnabled),         // gzip response
17✔
159
        )
17✔
160

17✔
161
        // no FQDNs defined, use the list of discovered servers
17✔
162
        if len(h.SSLConfig.FQDNs) == 0 && h.SSLConfig.SSLMode == SSLAuto {
17✔
UNCOV
163
                h.SSLConfig.FQDNs = h.discoveredServers(ctx, 50*time.Millisecond)
×
UNCOV
164
        }
×
165

166
        switch h.SSLConfig.SSLMode {
17✔
167
        case SSLNone:
16✔
168
                log.Printf("[INFO] activate http proxy server on %s", h.Address)
16✔
169
                httpServer = h.makeHTTPServer(h.Address, handler)
16✔
170
                httpServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
16✔
171
                if err := httpServer.ListenAndServe(); err != nil {
32✔
172
                        return fmt.Errorf("http proxy server failed: %w", err)
16✔
173
                }
16✔
UNCOV
174
                return nil
×
175
        case SSLStatic:
1✔
176
                log.Printf("[INFO] activate https server in 'static' mode on %s", h.Address)
1✔
177

1✔
178
                httpsServer = h.makeHTTPSServer(h.Address, handler)
1✔
179
                httpsServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
1✔
180

1✔
181
                httpServer = h.makeHTTPServer(h.toHTTP(h.Address, h.SSLConfig.RedirHTTPPort), h.httpToHTTPSRouter())
1✔
182
                httpServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
1✔
183

1✔
184
                go func() {
2✔
185
                        log.Printf("[INFO] activate http redirect server on %s", h.toHTTP(h.Address, h.SSLConfig.RedirHTTPPort))
1✔
186
                        err := httpServer.ListenAndServe()
1✔
187
                        log.Printf("[WARN] http redirect server terminated, %s", err)
1✔
188
                }()
1✔
189
                if err := httpsServer.ListenAndServeTLS(h.SSLConfig.Cert, h.SSLConfig.Key); err != nil {
2✔
190
                        return fmt.Errorf("https static server failed: %w", err)
1✔
191
                }
1✔
192
                return nil
×
193
        case SSLAuto:
×
194
                log.Printf("[INFO] activate https server in 'auto' mode on %s", h.Address)
×
195
                log.Printf("[DEBUG] FQDNs %v", h.SSLConfig.FQDNs)
×
196

×
197
                m := h.makeAutocertManager()
×
198
                httpsServer = h.makeHTTPSAutocertServer(h.Address, handler, m)
×
199
                httpsServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
×
200

×
201
                httpServer = h.makeHTTPServer(h.toHTTP(h.Address, h.SSLConfig.RedirHTTPPort), h.httpChallengeRouter(m))
×
202
                httpServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
×
203

×
UNCOV
204
                go func() {
×
205
                        log.Printf("[INFO] activate http challenge server on port %s", h.toHTTP(h.Address, h.SSLConfig.RedirHTTPPort))
×
206
                        err := httpServer.ListenAndServe()
×
207
                        log.Printf("[WARN] http challenge server terminated, %s", err)
×
208
                }()
×
209

210
                if err := httpsServer.ListenAndServeTLS("", ""); err != nil {
×
UNCOV
211
                        return fmt.Errorf("https auto server failed: %w", err)
×
UNCOV
212
                }
×
UNCOV
213
                return nil
×
214
        }
UNCOV
215
        return fmt.Errorf("unknown SSL type %v", h.SSLConfig.SSLMode)
×
216
}
217

218
type contextKey string
219

220
const (
221
        ctxURL       = contextKey("url")
222
        ctxMatchType = contextKey("type")
223
        ctxMatch     = contextKey("match")
224
        ctxKeepHost  = contextKey("keepHost")
225
)
226

227
func (h *Http) proxyHandler() http.HandlerFunc {
17✔
228

17✔
229
        reverseProxy := &httputil.ReverseProxy{
17✔
230
                Director: func(r *http.Request) {
38✔
231
                        ctx := r.Context()
21✔
232
                        uu := ctx.Value(ctxURL).(*url.URL)
21✔
233
                        keepHost := ctx.Value(ctxKeepHost).(bool)
21✔
234
                        r.Header.Add("X-Forwarded-Host", r.Host)
21✔
235
                        scheme := "http"
21✔
236
                        if h.SSLConfig.SSLMode == SSLAuto || h.SSLConfig.SSLMode == SSLStatic {
24✔
237
                                h.setHeaderIfNotExists(r, "X-Forwarded-Proto", "https")
3✔
238
                                h.setHeaderIfNotExists(r, "X-Forwarded-Port", "443")
3✔
239
                                scheme = "https"
3✔
240
                        }
3✔
241
                        r.Header.Set("X-Forwarded-URL", fmt.Sprintf("%s://%s%s", scheme, r.Host, r.URL.String()))
21✔
242
                        r.URL.Path = uu.Path
21✔
243
                        r.URL.Host = uu.Host
21✔
244
                        r.URL.Scheme = uu.Scheme
21✔
245
                        if !keepHost {
42✔
246
                                r.Host = uu.Host
21✔
247
                        } else {
21✔
UNCOV
248
                                log.Printf("[DEBUG] keep host %s", r.Host)
×
UNCOV
249
                        }
×
250
                        h.setXRealIP(r)
21✔
251
                },
252
                Transport: &http.Transport{
253
                        ResponseHeaderTimeout: h.Timeouts.ResponseHeader,
254
                        DialContext: (&net.Dialer{
255
                                Timeout:   h.Timeouts.Dial,
256
                                KeepAlive: h.Timeouts.KeepAlive,
257
                        }).DialContext,
258
                        ForceAttemptHTTP2:     true,
259
                        MaxIdleConns:          h.UpstreamMaxIdleConns,
260
                        MaxConnsPerHost:       h.UpstreamMaxConnsPerHost,
261
                        IdleConnTimeout:       h.Timeouts.IdleConn,
262
                        TLSHandshakeTimeout:   h.Timeouts.TLSHandshake,
263
                        ExpectContinueTimeout: h.Timeouts.ExpectContinue,
264
                        TLSClientConfig:       &tls.Config{InsecureSkipVerify: h.Insecure}, //nolint:gosec // g402: User defined option to disable verification for self-signed certificates
265
                },
266
                ErrorLog: log.ToStdLogger(log.Default(), "WARN"),
267
        }
268
        assetsHandler := h.assetsHandler()
17✔
269

17✔
270
        return func(w http.ResponseWriter, r *http.Request) {
53✔
271

36✔
272
                if r.Context().Value(ctxMatch) == nil { // no route match detected by matchHandler
46✔
273
                        if h.isAssetRequest(r) {
17✔
274
                                assetsHandler.ServeHTTP(w, r)
7✔
275
                                return
7✔
276
                        }
7✔
277
                        log.Printf("[WARN] no match for %s %s", r.URL.Hostname(), r.URL.Path)
3✔
278
                        h.Reporter.Report(w, http.StatusBadGateway)
3✔
279
                        return
3✔
280
                }
281

282
                match := r.Context().Value(ctxMatch).(discovery.MatchedRoute)
26✔
283
                matchType := r.Context().Value(ctxMatchType).(discovery.MatchType)
26✔
284

26✔
285
                switch matchType {
26✔
286
                case discovery.MTProxy:
23✔
287
                        switch match.Mapper.RedirectType {
23✔
288
                        case discovery.RTNone:
21✔
289
                                uu := r.Context().Value(ctxURL).(*url.URL)
21✔
290
                                log.Printf("[DEBUG] proxy to %s", uu)
21✔
291
                                reverseProxy.ServeHTTP(w, r)
21✔
292
                        case discovery.RTPerm:
1✔
293
                                log.Printf("[DEBUG] redirect (301) to %s", match.Destination)
1✔
294
                                http.Redirect(w, r, match.Destination, http.StatusMovedPermanently)
1✔
295
                        case discovery.RTTemp:
1✔
296
                                log.Printf("[DEBUG] redirect (302) to %s", match.Destination)
1✔
297
                                http.Redirect(w, r, match.Destination, http.StatusFound)
1✔
298
                        }
299

300
                case discovery.MTStatic:
3✔
301
                        // static match result has webroot:location:[spa:normal], i.e. /www:/var/somedir/:normal
3✔
302
                        ae := strings.Split(match.Destination, ":")
3✔
303
                        if len(ae) != 3 { // shouldn't happen
3✔
UNCOV
304
                                log.Printf("[WARN] unexpected static assets destination: %s", match.Destination)
×
305
                                h.Reporter.Report(w, http.StatusInternalServerError)
×
306
                                return
×
307
                        }
×
308
                        fs, err := h.fileServer(ae[0], ae[1], ae[2] == "spa", nil)
3✔
309
                        if err != nil {
3✔
UNCOV
310
                                log.Printf("[WARN] file server error, %v", err)
×
UNCOV
311
                                h.Reporter.Report(w, http.StatusInternalServerError)
×
UNCOV
312
                                return
×
UNCOV
313
                        }
×
314
                        h.CacheControl.Middleware(fs).ServeHTTP(w, r)
3✔
315
                }
316
        }
317
}
318

319
// matchHandler is a part of middleware chain. Matches incoming request to one or more matched rules
320
// and if match found sets it to the request context. Context used by proxy handler as well as by plugin conductor
321
func (h *Http) matchHandler(next http.Handler) http.Handler {
23✔
322

23✔
323
        getMatch := func(mm discovery.Matches, picker LBSelector) (m discovery.MatchedRoute, ok bool) {
74✔
324
                if len(mm.Routes) == 0 {
63✔
325
                        return m, false
12✔
326
                }
12✔
327

328
                var matches []discovery.MatchedRoute
39✔
329
                for _, m := range mm.Routes {
88✔
330
                        if m.Alive {
90✔
331
                                matches = append(matches, m)
41✔
332
                        }
41✔
333
                }
334
                switch len(matches) {
39✔
335
                case 0:
1✔
336
                        return m, false
1✔
337
                case 1:
36✔
338
                        return matches[0], true
36✔
339
                default:
2✔
340
                        return matches[picker.Select(len(matches))], true
2✔
341
                }
342
        }
343

344
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
74✔
345
                server := r.URL.Hostname()
51✔
346
                if server == "" {
96✔
347
                        server = strings.Split(r.Host, ":")[0] // drop port
45✔
348
                }
45✔
349
                matches := h.Match(server, r.URL.EscapedPath()) // get all matches for the server:path pair
51✔
350
                match, ok := getMatch(matches, h.LBSelector)
51✔
351
                if !ok {
64✔
352
                        next.ServeHTTP(w, r)
13✔
353
                        return
13✔
354
                }
13✔
355

356
                ctx := context.WithValue(r.Context(), ctxMatch, match)        // set match info
38✔
357
                ctx = context.WithValue(ctx, ctxMatchType, matches.MatchType) // set match type
38✔
358
                ctx = context.WithValue(ctx, plugin.CtxMatch, match)          // set match info for plugin conductor
38✔
359

38✔
360
                if matches.MatchType == discovery.MTProxy {
73✔
361
                        uu, err := url.Parse(match.Destination)
35✔
362
                        if err != nil {
35✔
UNCOV
363
                                log.Printf("[WARN] can't parse destination %s, %v", match.Destination, err)
×
UNCOV
364
                                h.Reporter.Report(w, http.StatusBadGateway)
×
365
                                return
×
366
                        }
×
367
                        ctx = context.WithValue(ctx, ctxURL, uu) // set destination url in request's context
35✔
368
                        keepHost := h.KeepHost
35✔
369
                        if match.Mapper.KeepHost != nil {
35✔
UNCOV
370
                                keepHost = *match.Mapper.KeepHost
×
UNCOV
371
                        }
×
372
                        ctx = context.WithValue(ctx, ctxKeepHost, keepHost) // set keep host in request's context
35✔
373
                }
374
                next.ServeHTTP(w, r.WithContext(ctx))
38✔
375
        })
376
}
377

378
func (h *Http) assetsHandler() http.HandlerFunc {
17✔
379
        if h.AssetsLocation == "" || h.AssetsWebRoot == "" {
31✔
380
                return func(_ http.ResponseWriter, _ *http.Request) {}
14✔
381
        }
382

383
        var notFound []byte
3✔
384
        var err error
3✔
385
        if h.Assets404 != "" {
4✔
386
                if notFound, err = os.ReadFile(filepath.Join(h.AssetsLocation, h.Assets404)); err != nil {
1✔
UNCOV
387
                        log.Printf("[WARN] can't read  404 file %s, %v", h.Assets404, err)
×
UNCOV
388
                        notFound = nil
×
UNCOV
389
                }
×
390
        }
391

392
        log.Printf("[DEBUG] shared assets server enabled for %s %s, spa=%v, not-found=%q",
3✔
393
                h.AssetsLocation, h.AssetsWebRoot, h.AssetsSPA, h.Assets404)
3✔
394

3✔
395
        fs, err := h.fileServer(h.AssetsWebRoot, h.AssetsLocation, h.AssetsSPA, notFound)
3✔
396
        if err != nil {
3✔
UNCOV
397
                log.Printf("[WARN] can't initialize assets server, %v", err)
×
UNCOV
398
                return func(_ http.ResponseWriter, _ *http.Request) {}
×
399
        }
400
        return h.CacheControl.Middleware(fs).ServeHTTP
3✔
401
}
402

403
func (h *Http) fileServer(assetsWebRoot, assetsLocation string, spa bool, notFound []byte) (http.Handler, error) {
6✔
404
        var notFoundReader io.Reader
6✔
405
        if notFound != nil {
7✔
406
                notFoundReader = bytes.NewReader(notFound)
1✔
407
        }
1✔
408
        var fs http.Handler
6✔
409
        var err error
6✔
410
        if spa {
8✔
411
                fs, err = R.NewFileServer(assetsWebRoot, assetsLocation, R.FsOptCustom404(notFoundReader), R.FsOptSPA)
2✔
412
        } else {
6✔
413
                fs, err = R.NewFileServer(assetsWebRoot, assetsLocation, R.FsOptCustom404(notFoundReader))
4✔
414
        }
4✔
415
        if err != nil {
6✔
UNCOV
416
                return nil, fmt.Errorf("failed to create file server: %w", err)
×
UNCOV
417
        }
×
418
        return fs, nil
6✔
419
}
420

421
func (h *Http) isAssetRequest(r *http.Request) bool {
17✔
422
        if h.AssetsLocation == "" || h.AssetsWebRoot == "" {
20✔
423
                return false
3✔
424
        }
3✔
425
        root := strings.TrimSuffix(h.AssetsWebRoot, "/")
14✔
426
        return r.URL.Path == root || strings.HasPrefix(r.URL.Path, root+"/")
14✔
427
}
428

429
func (h *Http) toHTTP(address string, httpPort int) string {
5✔
430
        rx := regexp.MustCompile(`(.*):(\d*)`)
5✔
431
        return rx.ReplaceAllString(address, "$1:") + strconv.Itoa(httpPort)
5✔
432
}
5✔
433

434
func (h *Http) pluginHandler() func(next http.Handler) http.Handler {
17✔
435
        if h.PluginConductor == nil {
34✔
436
                return passThroughHandler
17✔
437
        }
17✔
UNCOV
438
        log.Printf("[INFO] plugin support enabled")
×
439
        return h.PluginConductor.Middleware
×
440
}
441

442
func (h *Http) mgmtHandler() func(next http.Handler) http.Handler {
17✔
443
        if h.Metrics == nil {
17✔
UNCOV
444
                return passThroughHandler
×
NEW
445
        }
×
446
        log.Printf("[DEBUG] metrics enabled")
17✔
447
        return h.Metrics.Middleware
17✔
448
}
449

450
// basicAuthHandler provides global basic auth that skips routes with per-route auth configured.
451
func (h *Http) basicAuthHandler() func(next http.Handler) http.Handler {
17✔
452
        if !h.BasicAuthEnabled {
32✔
453
                return passThroughHandler
15✔
454
        }
15✔
455
        return globalBasicAuthHandler(h.BasicAuthAllowed)
2✔
456
}
457

458
func (h *Http) makeHTTPServer(addr string, router http.Handler) *http.Server {
18✔
459
        return &http.Server{
18✔
460
                Addr:              addr,
18✔
461
                Handler:           router,
18✔
462
                ReadHeaderTimeout: h.Timeouts.ReadHeader,
18✔
463
                WriteTimeout:      h.Timeouts.Write,
18✔
464
                IdleTimeout:       h.Timeouts.Idle,
18✔
465
        }
18✔
466
}
18✔
467

468
func (h *Http) setXRealIP(r *http.Request) {
21✔
469
        if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
21✔
UNCOV
470
                // use the left-most non-private client IP address
×
UNCOV
471
                // if there is no any non-private IP address, use the left-most address
×
UNCOV
472
                r.Header.Set("X-Real-IP", preferPublicIP(strings.Split(forwarded, ",")))
×
473
                return
×
474
        }
×
475

476
        ip, _, err := net.SplitHostPort(r.RemoteAddr)
21✔
477
        if err != nil {
21✔
478
                return
×
UNCOV
479
        }
×
480
        userIP := net.ParseIP(ip)
21✔
481
        if userIP == nil {
21✔
UNCOV
482
                return
×
UNCOV
483
        }
×
484
        r.Header.Set("X-Real-IP", ip)
21✔
485
}
486

487
// discoveredServers gets the list of servers discovered by providers.
488
// The underlying discovery is async and may happen not right away.
489
// We should try to get servers for some time and make sure we have the complete list of servers
490
// by checking if the number of servers has not changed between two calls.
491
func (h *Http) discoveredServers(ctx context.Context, interval time.Duration) (servers []string) {
1✔
492
        discoveredServers := 0
1✔
493

1✔
494
        for range 100 {
9✔
495
                select {
8✔
UNCOV
496
                case <-ctx.Done():
×
UNCOV
497
                        return nil
×
498
                default:
8✔
499
                }
500
                servers = h.Servers() // fill all discovered if nothing defined
8✔
501
                if len(servers) > 0 && len(servers) == discoveredServers {
9✔
502
                        break
1✔
503
                }
504
                discoveredServers = len(servers)
7✔
505
                time.Sleep(interval)
7✔
506
        }
507
        return servers
1✔
508
}
509

510
func (h *Http) setHeaderIfNotExists(r *http.Request, key, value string) {
6✔
511
        if _, ok := r.Header[key]; !ok {
8✔
512
                r.Header.Set(key, value)
2✔
513
        }
2✔
514
}
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