Browse code

Use scheme/host from request for token redirect

Jordan Liggitt authored on 2016/08/16 01:30:03
Showing 6 changed files
... ...
@@ -13,6 +13,12 @@ storage:
13 13
 auth:
14 14
   openshift:
15 15
     realm: openshift
16
+    
17
+    # tokenrealm is a base URL to use for the token-granting registry endpoint.
18
+    # If unspecified, the scheme and host for the token redirect are determined from the incoming request.
19
+    # If specified, a scheme and host must be chosen that all registry clients can resolve and access:
20
+    #
21
+    # tokenrealm: https://example.com:5000
16 22
 middleware:
17 23
   registry:
18 24
     - name: openshift
... ...
@@ -7,7 +7,6 @@ import (
7 7
 	"io"
8 8
 	"io/ioutil"
9 9
 	"net/http"
10
-	"net/url"
11 10
 	"os"
12 11
 	"time"
13 12
 
... ...
@@ -47,28 +46,6 @@ func Execute(configFile io.Reader) {
47 47
 		log.Fatalf("Error parsing configuration file: %s", err)
48 48
 	}
49 49
 
50
-	tokenPath := "/openshift/token"
51
-
52
-	// If needed, generate and populate the token realm URL in the config.
53
-	// Must be done prior to instantiating the app, so our auth provider has the config available.
54
-	_, usingOpenShiftAuth := config.Auth[server.OpenShiftAuth]
55
-	_, hasTokenRealm := config.Auth[server.OpenShiftAuth][server.TokenRealmKey].(string)
56
-	if usingOpenShiftAuth && !hasTokenRealm {
57
-		registryHost := os.Getenv(server.DockerRegistryURLEnvVar)
58
-		if len(registryHost) == 0 {
59
-			log.Fatalf("%s is required", server.DockerRegistryURLEnvVar)
60
-		}
61
-		tokenURL := &url.URL{Scheme: "https", Host: registryHost, Path: tokenPath}
62
-		if len(config.HTTP.TLS.Certificate) == 0 {
63
-			tokenURL.Scheme = "http"
64
-		}
65
-
66
-		if config.Auth[server.OpenShiftAuth] == nil {
67
-			config.Auth[server.OpenShiftAuth] = configuration.Parameters{}
68
-		}
69
-		config.Auth[server.OpenShiftAuth][server.TokenRealmKey] = tokenURL.String()
70
-	}
71
-
72 50
 	ctx := context.Background()
73 51
 	ctx, err = configureLogging(ctx, config)
74 52
 	if err != nil {
... ...
@@ -82,8 +59,16 @@ func Execute(configFile io.Reader) {
82 82
 	app := handlers.NewApp(ctx, config)
83 83
 
84 84
 	// Add a token handling endpoint
85
-	if usingOpenShiftAuth {
86
-		app.NewRoute().Methods("GET").PathPrefix(tokenPath).Handler(server.NewTokenHandler(ctx, server.DefaultRegistryClient))
85
+	if options, usingOpenShiftAuth := config.Auth[server.OpenShiftAuth]; usingOpenShiftAuth {
86
+		tokenRealm, err := server.TokenRealm(options)
87
+		if err != nil {
88
+			log.Fatalf("error setting up token auth: %s", err)
89
+		}
90
+		err = app.NewRoute().Methods("GET").PathPrefix(tokenRealm.Path).Handler(server.NewTokenHandler(ctx, server.DefaultRegistryClient)).GetError()
91
+		if err != nil {
92
+			log.Fatalf("error setting up token endpoint at %q: %v", tokenRealm.Path, err)
93
+		}
94
+		log.Debugf("configured token endpoint at %q", tokenRealm.String())
87 95
 	}
88 96
 
89 97
 	// TODO add https scheme
... ...
@@ -4,6 +4,7 @@ import (
4 4
 	"errors"
5 5
 	"fmt"
6 6
 	"net/http"
7
+	"net/url"
7 8
 	"strings"
8 9
 
9 10
 	log "github.com/Sirupsen/logrus"
... ...
@@ -18,6 +19,7 @@ import (
18 18
 	"github.com/openshift/origin/pkg/client"
19 19
 	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
20 20
 	imageapi "github.com/openshift/origin/pkg/image/api"
21
+	"github.com/openshift/origin/pkg/util/httprequest"
21 22
 )
22 23
 
23 24
 type deferredErrors map[string]error
... ...
@@ -36,8 +38,10 @@ func (d deferredErrors) Empty() bool {
36 36
 const (
37 37
 	OpenShiftAuth = "openshift"
38 38
 
39
+	defaultTokenPath = "/openshift/token"
40
+
39 41
 	RealmKey      = "realm"
40
-	TokenRealmKey = "token-realm"
42
+	TokenRealmKey = "tokenrealm"
41 43
 )
42 44
 
43 45
 // RegistryClient encapsulates getting access to the OpenShift API.
... ...
@@ -113,7 +117,7 @@ func DeferredErrorsFrom(ctx context.Context) (deferredErrors, bool) {
113 113
 
114 114
 type AccessController struct {
115 115
 	realm      string
116
-	tokenRealm string
116
+	tokenRealm *url.URL
117 117
 	config     restclient.Config
118 118
 }
119 119
 
... ...
@@ -147,6 +151,36 @@ var (
147 147
 	ErrUnsupportedResource = errors.New("unsupported resource")
148 148
 )
149 149
 
150
+// TokenRealm returns the template URL to use as the token realm redirect.
151
+// An empty scheme/host in the returned URL means to match the scheme/host on incoming requests.
152
+func TokenRealm(options map[string]interface{}) (*url.URL, error) {
153
+	if options[TokenRealmKey] == nil {
154
+		// If not specified, default to "/openshift/token", auto-detecting the scheme and host
155
+		return &url.URL{Path: defaultTokenPath}, nil
156
+	}
157
+
158
+	tokenRealmString, ok := options[TokenRealmKey].(string)
159
+	if !ok {
160
+		return nil, fmt.Errorf("%s config option must be a string, got %T", TokenRealmKey, options[TokenRealmKey])
161
+	}
162
+
163
+	tokenRealm, err := url.Parse(tokenRealmString)
164
+	if err != nil {
165
+		return nil, fmt.Errorf("error parsing URL in %s config option: %v", TokenRealmKey, err)
166
+	}
167
+	if len(tokenRealm.RawQuery) > 0 || len(tokenRealm.Fragment) > 0 {
168
+		return nil, fmt.Errorf("%s config option may not contain query parameters or a fragment", TokenRealmKey)
169
+	}
170
+	if len(tokenRealm.Path) > 0 {
171
+		return nil, fmt.Errorf("%s config option may not contain a path (%q was specified)", TokenRealmKey, tokenRealm.Path)
172
+	}
173
+
174
+	// pin to "/openshift/token"
175
+	tokenRealm.Path = defaultTokenPath
176
+
177
+	return tokenRealm, nil
178
+}
179
+
150 180
 func newAccessController(options map[string]interface{}) (registryauth.AccessController, error) {
151 181
 	log.Info("Using Origin Auth handler")
152 182
 	realm, ok := options[RealmKey].(string)
... ...
@@ -155,7 +189,10 @@ func newAccessController(options map[string]interface{}) (registryauth.AccessCon
155 155
 		realm = "origin"
156 156
 	}
157 157
 
158
-	tokenRealm, _ := options[TokenRealmKey].(string)
158
+	tokenRealm, err := TokenRealm(options)
159
+	if err != nil {
160
+		return nil, err
161
+	}
159 162
 
160 163
 	return &AccessController{realm: realm, tokenRealm: tokenRealm, config: DefaultRegistryClient.SafeClientConfig()}, nil
161 164
 }
... ...
@@ -193,17 +230,34 @@ func (ac *tokenAuthChallenge) SetHeaders(w http.ResponseWriter) {
193 193
 }
194 194
 
195 195
 // wrapErr wraps errors related to authorization in an authChallenge error that will present a WWW-Authenticate challenge response
196
-func (ac *AccessController) wrapErr(err error) error {
196
+func (ac *AccessController) wrapErr(ctx context.Context, err error) error {
197 197
 	switch err {
198 198
 	case ErrTokenRequired:
199 199
 		// Challenge for errors that involve missing tokens
200
-		if len(ac.tokenRealm) > 0 {
201
-			// Direct to token auth if we've been given a place to direct to
202
-			return &tokenAuthChallenge{realm: ac.tokenRealm, err: err}
203
-		} else {
204
-			// Otherwise just send the basic challenge
200
+		if ac.tokenRealm == nil {
201
+			// Send the basic challenge if we don't have a place to redirect
205 202
 			return &authChallenge{realm: ac.realm, err: err}
206 203
 		}
204
+
205
+		if len(ac.tokenRealm.Scheme) > 0 && len(ac.tokenRealm.Host) > 0 {
206
+			// Redirect to token auth if we've been given an absolute URL
207
+			return &tokenAuthChallenge{realm: ac.tokenRealm.String(), err: err}
208
+		}
209
+
210
+		// Auto-detect scheme/host from request
211
+		req, reqErr := context.GetRequest(ctx)
212
+		if reqErr != nil {
213
+			return reqErr
214
+		}
215
+		scheme, host := httprequest.SchemeHost(req)
216
+		tokenRealmCopy := *ac.tokenRealm
217
+		if len(tokenRealmCopy.Scheme) == 0 {
218
+			tokenRealmCopy.Scheme = scheme
219
+		}
220
+		if len(tokenRealmCopy.Host) == 0 {
221
+			tokenRealmCopy.Host = host
222
+		}
223
+		return &tokenAuthChallenge{realm: tokenRealmCopy.String(), err: err}
207 224
 	case ErrTokenInvalid, ErrOpenShiftAccessDenied:
208 225
 		// Challenge for errors that involve tokens or access denied
209 226
 		return &authChallenge{realm: ac.realm, err: err}
... ...
@@ -224,25 +278,25 @@ func (ac *AccessController) wrapErr(err error) error {
224 224
 func (ac *AccessController) Authorized(ctx context.Context, accessRecords ...registryauth.Access) (context.Context, error) {
225 225
 	req, err := context.GetRequest(ctx)
226 226
 	if err != nil {
227
-		return nil, ac.wrapErr(err)
227
+		return nil, ac.wrapErr(ctx, err)
228 228
 	}
229 229
 
230 230
 	bearerToken, err := getOpenShiftAPIToken(ctx, req)
231 231
 	if err != nil {
232
-		return nil, ac.wrapErr(err)
232
+		return nil, ac.wrapErr(ctx, err)
233 233
 	}
234 234
 
235 235
 	copied := ac.config
236 236
 	copied.BearerToken = bearerToken
237 237
 	osClient, err := client.New(&copied)
238 238
 	if err != nil {
239
-		return nil, ac.wrapErr(err)
239
+		return nil, ac.wrapErr(ctx, err)
240 240
 	}
241 241
 
242 242
 	// In case of docker login, hits endpoint /v2
243 243
 	if len(accessRecords) == 0 {
244 244
 		if err := verifyOpenShiftUser(ctx, osClient); err != nil {
245
-			return nil, ac.wrapErr(err)
245
+			return nil, ac.wrapErr(ctx, err)
246 246
 		}
247 247
 	}
248 248
 
... ...
@@ -262,7 +316,7 @@ func (ac *AccessController) Authorized(ctx context.Context, accessRecords ...reg
262 262
 		case "repository":
263 263
 			imageStreamNS, imageStreamName, err := getNamespaceName(access.Resource.Name)
264 264
 			if err != nil {
265
-				return nil, ac.wrapErr(err)
265
+				return nil, ac.wrapErr(ctx, err)
266 266
 			}
267 267
 
268 268
 			verb := ""
... ...
@@ -275,7 +329,7 @@ func (ac *AccessController) Authorized(ctx context.Context, accessRecords ...reg
275 275
 			case "*":
276 276
 				verb = "prune"
277 277
 			default:
278
-				return nil, ac.wrapErr(ErrUnsupportedAction)
278
+				return nil, ac.wrapErr(ctx, ErrUnsupportedAction)
279 279
 			}
280 280
 
281 281
 			switch verb {
... ...
@@ -284,15 +338,15 @@ func (ac *AccessController) Authorized(ctx context.Context, accessRecords ...reg
284 284
 					continue
285 285
 				}
286 286
 				if err := verifyPruneAccess(ctx, osClient); err != nil {
287
-					return nil, ac.wrapErr(err)
287
+					return nil, ac.wrapErr(ctx, err)
288 288
 				}
289 289
 				verifiedPrune = true
290 290
 			default:
291 291
 				if err := verifyImageStreamAccess(ctx, imageStreamNS, imageStreamName, verb, osClient); err != nil {
292 292
 					if access.Action != "pull" {
293
-						return nil, ac.wrapErr(err)
293
+						return nil, ac.wrapErr(ctx, err)
294 294
 					}
295
-					possibleCrossMountErrors.Add(imageStreamNS, imageStreamName, ac.wrapErr(err))
295
+					possibleCrossMountErrors.Add(imageStreamNS, imageStreamName, ac.wrapErr(ctx, err))
296 296
 				}
297 297
 			}
298 298
 
... ...
@@ -303,14 +357,14 @@ func (ac *AccessController) Authorized(ctx context.Context, accessRecords ...reg
303 303
 					continue
304 304
 				}
305 305
 				if err := verifyPruneAccess(ctx, osClient); err != nil {
306
-					return nil, ac.wrapErr(err)
306
+					return nil, ac.wrapErr(ctx, err)
307 307
 				}
308 308
 				verifiedPrune = true
309 309
 			default:
310
-				return nil, ac.wrapErr(ErrUnsupportedAction)
310
+				return nil, ac.wrapErr(ctx, ErrUnsupportedAction)
311 311
 			}
312 312
 		default:
313
-			return nil, ac.wrapErr(ErrUnsupportedResource)
313
+			return nil, ac.wrapErr(ctx, ErrUnsupportedResource)
314 314
 		}
315 315
 	}
316 316
 
... ...
@@ -1,10 +1,12 @@
1 1
 package server
2 2
 
3 3
 import (
4
+	"crypto/tls"
4 5
 	"errors"
5 6
 	"fmt"
6 7
 	"net/http"
7 8
 	"net/http/httptest"
9
+	"net/url"
8 10
 	"reflect"
9 11
 	"testing"
10 12
 
... ...
@@ -84,14 +86,15 @@ func TestVerifyImageStreamAccess(t *testing.T) {
84 84
 
85 85
 // TestAccessController tests complete integration of the v2 registry auth package.
86 86
 func TestAccessController(t *testing.T) {
87
-	options := map[string]interface{}{
87
+	defaultOptions := map[string]interface{}{
88 88
 		"addr":        "https://openshift-example.com/osapi",
89 89
 		"apiVersion":  latest.Version,
90 90
 		RealmKey:      "myrealm",
91
-		TokenRealmKey: "https://tokenrealm.com/token",
91
+		TokenRealmKey: "http://tokenrealm.com",
92 92
 	}
93 93
 
94 94
 	tests := map[string]struct {
95
+		options            map[string]interface{}
95 96
 		access             []auth.Access
96 97
 		basicToken         string
97 98
 		bearerToken        string
... ...
@@ -107,7 +110,20 @@ func TestAccessController(t *testing.T) {
107 107
 			basicToken:        "",
108 108
 			expectedError:     ErrTokenRequired,
109 109
 			expectedChallenge: true,
110
-			expectedHeaders:   http.Header{"Www-Authenticate": []string{`Bearer realm="https://tokenrealm.com/token"`}},
110
+			expectedHeaders:   http.Header{"Www-Authenticate": []string{`Bearer realm="http://tokenrealm.com/openshift/token"`}},
111
+		},
112
+		"no token, autodetected tokenrealm": {
113
+			options: map[string]interface{}{
114
+				"addr":        "https://openshift-example.com/osapi",
115
+				"apiVersion":  latest.Version,
116
+				RealmKey:      "myrealm",
117
+				TokenRealmKey: "",
118
+			},
119
+			access:            []auth.Access{},
120
+			basicToken:        "",
121
+			expectedError:     ErrTokenRequired,
122
+			expectedChallenge: true,
123
+			expectedHeaders:   http.Header{"Www-Authenticate": []string{`Bearer realm="https://openshift-example.com/openshift/token"`}},
111 124
 		},
112 125
 		"invalid registry token": {
113 126
 			access: []auth.Access{{
... ...
@@ -317,11 +333,22 @@ func TestAccessController(t *testing.T) {
317 317
 	}
318 318
 
319 319
 	for k, test := range tests {
320
+		options := test.options
321
+		if options == nil {
322
+			options = defaultOptions
323
+		}
324
+		reqURL, err := url.Parse(options["addr"].(string))
325
+		if err != nil {
326
+			t.Fatal(err)
327
+		}
320 328
 		req, err := http.NewRequest("GET", options["addr"].(string), nil)
321 329
 		if err != nil {
322 330
 			t.Errorf("%s: %v", k, err)
323 331
 			continue
324 332
 		}
333
+		// Simulate a secure request to the specified server
334
+		req.Host = reqURL.Host
335
+		req.TLS = &tls.ConnectionState{ServerName: reqURL.Host}
325 336
 		if len(test.basicToken) > 0 {
326 337
 			req.Header.Set("Authorization", fmt.Sprintf("Basic %s", test.basicToken))
327 338
 		}
... ...
@@ -1,6 +1,7 @@
1 1
 package httprequest
2 2
 
3 3
 import (
4
+	"net"
4 5
 	"net/http"
5 6
 	"strings"
6 7
 
... ...
@@ -38,3 +39,72 @@ func PrefersHTML(req *http.Request) bool {
38 38
 
39 39
 	return false
40 40
 }
41
+
42
+// SchemeHost returns the scheme and host used to make this request.
43
+// Suitable for use to compute scheme/host in returned 302 redirect Location.
44
+// Note the returned host is not normalized, and may or may not contain a port.
45
+// Returned values are based on the following information:
46
+//
47
+// Host:
48
+// * X-Forwarded-Host/X-Forwarded-Port headers
49
+// * Host field on the request (parsed from Host header)
50
+// * Host in the request's URL (parsed from Request-Line)
51
+//
52
+// Scheme:
53
+// * X-Forwarded-Proto header
54
+// * Existence of TLS information on the request implies https
55
+// * Scheme in the request's URL (parsed from Request-Line)
56
+// * Port (if included in calculated Host value, 443 implies https)
57
+// * Otherwise, defaults to "http"
58
+func SchemeHost(req *http.Request) (string /*scheme*/, string /*host*/) {
59
+	forwarded := func(attr string) string {
60
+		// Get the X-Forwarded-<attr> value
61
+		value := req.Header.Get("X-Forwarded-" + attr)
62
+		// Take the first comma-separated value, if multiple exist
63
+		value = strings.SplitN(value, ",", 2)[0]
64
+		// Trim whitespace
65
+		return strings.TrimSpace(value)
66
+	}
67
+
68
+	forwardedProto := forwarded("Proto")
69
+	forwardedHost := forwarded("Host")
70
+	// If both X-Forwarded-Host and X-Forwarded-Port are sent, use the explicit port info
71
+	if forwardedPort := forwarded("Port"); len(forwardedHost) > 0 && len(forwardedPort) > 0 {
72
+		if h, _, err := net.SplitHostPort(forwardedHost); err == nil {
73
+			forwardedHost = net.JoinHostPort(h, forwardedPort)
74
+		} else {
75
+			forwardedHost = net.JoinHostPort(forwardedHost, forwardedPort)
76
+		}
77
+	}
78
+
79
+	host := ""
80
+	switch {
81
+	case len(forwardedHost) > 0:
82
+		host = forwardedHost
83
+	case len(req.Host) > 0:
84
+		host = req.Host
85
+	case len(req.URL.Host) > 0:
86
+		host = req.URL.Host
87
+	}
88
+
89
+	port := ""
90
+	if _, p, err := net.SplitHostPort(host); err == nil {
91
+		port = p
92
+	}
93
+
94
+	scheme := ""
95
+	switch {
96
+	case len(forwardedProto) > 0:
97
+		scheme = forwardedProto
98
+	case req.TLS != nil:
99
+		scheme = "https"
100
+	case len(req.URL.Scheme) > 0:
101
+		scheme = req.URL.Scheme
102
+	case port == "443":
103
+		scheme = "https"
104
+	default:
105
+		scheme = "http"
106
+	}
107
+
108
+	return scheme, host
109
+}
41 110
new file mode 100644
... ...
@@ -0,0 +1,196 @@
0
+package httprequest
1
+
2
+import (
3
+	"crypto/tls"
4
+	"net/http"
5
+	"net/url"
6
+	"testing"
7
+)
8
+
9
+func TestSchemeHost(t *testing.T) {
10
+
11
+	testcases := map[string]struct {
12
+		req            *http.Request
13
+		expectedScheme string
14
+		expectedHost   string
15
+	}{
16
+		"X-Forwarded-Host and X-Forwarded-Port combined": {
17
+			req: &http.Request{
18
+				URL:  &url.URL{Path: "/"},
19
+				Host: "127.0.0.1",
20
+				Header: http.Header{
21
+					"X-Forwarded-Host":  []string{"example.com"},
22
+					"X-Forwarded-Port":  []string{"443"},
23
+					"X-Forwarded-Proto": []string{"https"},
24
+				},
25
+			},
26
+			expectedScheme: "https",
27
+			expectedHost:   "example.com:443",
28
+		},
29
+		"X-Forwarded-Port overwrites X-Forwarded-Host port": {
30
+			req: &http.Request{
31
+				URL:  &url.URL{Path: "/"},
32
+				Host: "127.0.0.1",
33
+				Header: http.Header{
34
+					"X-Forwarded-Host":  []string{"example.com:1234"},
35
+					"X-Forwarded-Port":  []string{"443"},
36
+					"X-Forwarded-Proto": []string{"https"},
37
+				},
38
+			},
39
+			expectedScheme: "https",
40
+			expectedHost:   "example.com:443",
41
+		},
42
+		"X-Forwarded-* multiple attrs": {
43
+			req: &http.Request{
44
+				URL:  &url.URL{Host: "urlhost", Path: "/"},
45
+				Host: "reqhost",
46
+				Header: http.Header{
47
+					"X-Forwarded-Host":  []string{"example.com,foo.com"},
48
+					"X-Forwarded-Port":  []string{"443,123"},
49
+					"X-Forwarded-Proto": []string{"https,http"},
50
+				},
51
+			},
52
+			expectedScheme: "https",
53
+			expectedHost:   "example.com:443",
54
+		},
55
+
56
+		"req host": {
57
+			req: &http.Request{
58
+				URL:  &url.URL{Host: "urlhost", Path: "/"},
59
+				Host: "example.com",
60
+			},
61
+			expectedScheme: "http",
62
+			expectedHost:   "example.com",
63
+		},
64
+		"req host with port": {
65
+			req: &http.Request{
66
+				URL:  &url.URL{Host: "urlhost", Path: "/"},
67
+				Host: "example.com:80",
68
+			},
69
+			expectedScheme: "http",
70
+			expectedHost:   "example.com:80",
71
+		},
72
+		"req host with tls port": {
73
+			req: &http.Request{
74
+				URL:  &url.URL{Host: "urlhost", Path: "/"},
75
+				Host: "example.com:443",
76
+			},
77
+			expectedScheme: "https",
78
+			expectedHost:   "example.com:443",
79
+		},
80
+
81
+		"req tls": {
82
+			req: &http.Request{
83
+				URL:  &url.URL{Path: "/"},
84
+				Host: "example.com",
85
+				TLS:  &tls.ConnectionState{},
86
+			},
87
+			expectedScheme: "https",
88
+			expectedHost:   "example.com",
89
+		},
90
+
91
+		"req url": {
92
+			req: &http.Request{
93
+				URL: &url.URL{Scheme: "https", Host: "example.com", Path: "/"},
94
+			},
95
+			expectedScheme: "https",
96
+			expectedHost:   "example.com",
97
+		},
98
+		"req url with port": {
99
+			req: &http.Request{
100
+				URL: &url.URL{Scheme: "https", Host: "example.com:123", Path: "/"},
101
+			},
102
+			expectedScheme: "https",
103
+			expectedHost:   "example.com:123",
104
+		},
105
+
106
+		// The following scenarios are captured from actual direct requests to pods
107
+		"non-tls pod": {
108
+			req: &http.Request{
109
+				URL:  &url.URL{Path: "/"},
110
+				Host: "172.17.0.2:9080",
111
+			},
112
+			expectedScheme: "http",
113
+			expectedHost:   "172.17.0.2:9080",
114
+		},
115
+		"tls pod": {
116
+			req: &http.Request{
117
+				URL:  &url.URL{Path: "/"},
118
+				Host: "172.17.0.2:9443",
119
+				TLS:  &tls.ConnectionState{ /* request has non-nil TLS connection state */ },
120
+			},
121
+			expectedScheme: "https",
122
+			expectedHost:   "172.17.0.2:9443",
123
+		},
124
+
125
+		// The following scenarios are captured from actual requests to pods via services
126
+		"svc -> non-tls pod": {
127
+			req: &http.Request{
128
+				URL:  &url.URL{Path: "/"},
129
+				Host: "service.default.svc.cluster.local:10080",
130
+			},
131
+			expectedScheme: "http",
132
+			expectedHost:   "service.default.svc.cluster.local:10080",
133
+		},
134
+		"svc -> tls pod": {
135
+			req: &http.Request{
136
+				URL:  &url.URL{Path: "/"},
137
+				Host: "service.default.svc.cluster.local:10443",
138
+				TLS:  &tls.ConnectionState{ /* request has non-nil TLS connection state */ },
139
+			},
140
+			expectedScheme: "https",
141
+			expectedHost:   "service.default.svc.cluster.local:10443",
142
+		},
143
+
144
+		// The following scenarios are captured from actual requests to pods via services via routes serviced by haproxy
145
+		"haproxy non-tls route -> svc -> non-tls pod": {
146
+			req: &http.Request{
147
+				URL:  &url.URL{Path: "/"},
148
+				Host: "route-namespace.router.default.svc.cluster.local",
149
+				Header: http.Header{
150
+					"X-Forwarded-Host":  []string{"route-namespace.router.default.svc.cluster.local"},
151
+					"X-Forwarded-Port":  []string{"80"},
152
+					"X-Forwarded-Proto": []string{"http"},
153
+					"Forwarded":         []string{"for=172.18.2.57;host=route-namespace.router.default.svc.cluster.local;proto=http"},
154
+					"X-Forwarded-For":   []string{"172.18.2.57"},
155
+				},
156
+			},
157
+			expectedScheme: "http",
158
+			expectedHost:   "route-namespace.router.default.svc.cluster.local:80",
159
+		},
160
+		"haproxy edge terminated route -> svc -> non-tls pod": {
161
+			req: &http.Request{
162
+				URL:  &url.URL{Path: "/"},
163
+				Host: "route-namespace.router.default.svc.cluster.local",
164
+				Header: http.Header{
165
+					"X-Forwarded-Host":  []string{"route-namespace.router.default.svc.cluster.local"},
166
+					"X-Forwarded-Port":  []string{"443"},
167
+					"X-Forwarded-Proto": []string{"https"},
168
+					"Forwarded":         []string{"for=172.18.2.57;host=route-namespace.router.default.svc.cluster.local;proto=https"},
169
+					"X-Forwarded-For":   []string{"172.18.2.57"},
170
+				},
171
+			},
172
+			expectedScheme: "https",
173
+			expectedHost:   "route-namespace.router.default.svc.cluster.local:443",
174
+		},
175
+		"haproxy passthrough route -> svc -> tls pod": {
176
+			req: &http.Request{
177
+				URL:  &url.URL{Path: "/"},
178
+				Host: "route-namespace.router.default.svc.cluster.local",
179
+				TLS:  &tls.ConnectionState{ /* request has non-nil TLS connection state */ },
180
+			},
181
+			expectedScheme: "https",
182
+			expectedHost:   "route-namespace.router.default.svc.cluster.local",
183
+		},
184
+	}
185
+
186
+	for k, tc := range testcases {
187
+		scheme, host := SchemeHost(tc.req)
188
+		if scheme != tc.expectedScheme {
189
+			t.Errorf("%s: expected scheme %q, got %q", k, tc.expectedScheme, scheme)
190
+		}
191
+		if host != tc.expectedHost {
192
+			t.Errorf("%s: expected host %q, got %q", k, tc.expectedHost, host)
193
+		}
194
+	}
195
+}