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

umputun / reproxy / 20125541625

11 Dec 2025 07:36AM UTC coverage: 78.335% (+0.4%) from 77.945%
20125541625

push

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

56 of 56 new or added lines in 3 files covered. (100.0%)

91 existing lines in 6 files now uncovered.

2343 of 2991 relevant lines covered (78.34%)

21.51 hits per line

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

78.53
/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
        BasicAuthEnabled bool
54
        BasicAuthAllowed []string
55

56
        ThrottleSystem int
57
        ThrottleUser   int
58

59
        KeepHost bool
60

61
        UpstreamMaxIdleConns    int
62
        UpstreamMaxConnsPerHost int
63

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

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

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

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

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

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

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

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

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

120
        var httpServer, httpsServer *http.Server
17✔
121

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

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

17✔
156
        // no FQDNs defined, use the list of discovered servers
17✔
157
        if len(h.SSLConfig.FQDNs) == 0 && h.SSLConfig.SSLMode == SSLAuto {
17✔
UNCOV
158
                h.SSLConfig.FQDNs = h.discoveredServers(ctx, 50*time.Millisecond)
×
UNCOV
159
        }
×
160

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

1✔
173
                httpsServer = h.makeHTTPSServer(h.Address, handler)
1✔
174
                httpsServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
1✔
175

1✔
176
                httpServer = h.makeHTTPServer(h.toHTTP(h.Address, h.SSLConfig.RedirHTTPPort), h.httpToHTTPSRouter())
1✔
177
                httpServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
1✔
178

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

×
192
                m := h.makeAutocertManager()
×
193
                httpsServer = h.makeHTTPSAutocertServer(h.Address, handler, m)
×
194
                httpsServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
×
195

×
196
                httpServer = h.makeHTTPServer(h.toHTTP(h.Address, h.SSLConfig.RedirHTTPPort), h.httpChallengeRouter(m))
×
197
                httpServer.ErrorLog = log.ToStdLogger(log.Default(), "WARN")
×
UNCOV
198

×
199
                go func() {
×
200
                        log.Printf("[INFO] activate http challenge server on port %s", h.toHTTP(h.Address, h.SSLConfig.RedirHTTPPort))
×
201
                        err := httpServer.ListenAndServe()
×
202
                        log.Printf("[WARN] http challenge server terminated, %s", err)
×
UNCOV
203
                }()
×
204

UNCOV
205
                if err := httpsServer.ListenAndServeTLS("", ""); err != nil {
×
UNCOV
206
                        return fmt.Errorf("https auto server failed: %w", err)
×
UNCOV
207
                }
×
UNCOV
208
                return nil
×
209
        }
UNCOV
210
        return fmt.Errorf("unknown SSL type %v", h.SSLConfig.SSLMode)
×
211
}
212

213
type contextKey string
214

215
const (
216
        ctxURL       = contextKey("url")
217
        ctxMatchType = contextKey("type")
218
        ctxMatch     = contextKey("match")
219
        ctxKeepHost  = contextKey("keepHost")
220
)
221

222
func (h *Http) proxyHandler() http.HandlerFunc {
17✔
223

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

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

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

277
                match := r.Context().Value(ctxMatch).(discovery.MatchedRoute)
26✔
278
                matchType := r.Context().Value(ctxMatchType).(discovery.MatchType)
26✔
279

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

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

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

23✔
318
        getMatch := func(mm discovery.Matches, picker LBSelector) (m discovery.MatchedRoute, ok bool) {
74✔
319
                if len(mm.Routes) == 0 {
63✔
320
                        return m, false
12✔
321
                }
12✔
322

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

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

351
                ctx := context.WithValue(r.Context(), ctxMatch, match)        // set match info
38✔
352
                ctx = context.WithValue(ctx, ctxMatchType, matches.MatchType) // set match type
38✔
353
                ctx = context.WithValue(ctx, plugin.CtxMatch, match)          // set match info for plugin conductor
38✔
354

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

373
func (h *Http) assetsHandler() http.HandlerFunc {
17✔
374
        if h.AssetsLocation == "" || h.AssetsWebRoot == "" {
31✔
375
                return func(_ http.ResponseWriter, _ *http.Request) {}
14✔
376
        }
377

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

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

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

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

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

424
func (h *Http) toHTTP(address string, httpPort int) string {
5✔
425
        rx := regexp.MustCompile(`(.*):(\d*)`)
5✔
426
        return rx.ReplaceAllString(address, "$1:") + strconv.Itoa(httpPort)
5✔
427
}
5✔
428

429
func (h *Http) pluginHandler() func(next http.Handler) http.Handler {
17✔
430
        if h.PluginConductor == nil {
34✔
431
                return passThroughHandler
17✔
432
        }
17✔
433
        log.Printf("[INFO] plugin support enabled")
×
434
        return h.PluginConductor.Middleware
×
435
}
436

437
func (h *Http) mgmtHandler() func(next http.Handler) http.Handler {
17✔
438
        if h.Metrics == nil {
17✔
UNCOV
439
                return passThroughHandler
×
UNCOV
440
        }
×
441
        log.Printf("[DEBUG] metrics enabled")
17✔
442
        return h.Metrics.Middleware
17✔
443
}
444

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

453
func (h *Http) makeHTTPServer(addr string, router http.Handler) *http.Server {
18✔
454
        return &http.Server{
18✔
455
                Addr:              addr,
18✔
456
                Handler:           router,
18✔
457
                ReadHeaderTimeout: h.Timeouts.ReadHeader,
18✔
458
                WriteTimeout:      h.Timeouts.Write,
18✔
459
                IdleTimeout:       h.Timeouts.Idle,
18✔
460
        }
18✔
461
}
18✔
462

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

471
        ip, _, err := net.SplitHostPort(r.RemoteAddr)
21✔
472
        if err != nil {
21✔
UNCOV
473
                return
×
UNCOV
474
        }
×
475
        userIP := net.ParseIP(ip)
21✔
476
        if userIP == nil {
21✔
477
                return
×
478
        }
×
479
        r.Header.Set("X-Real-IP", ip)
21✔
480
}
481

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

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

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