| ... | ... |
@@ -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 |
+} |