diff --git a/configuration/configuration.go b/configuration/configuration.go index 6712f3d82..3dff32f84 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -64,6 +64,10 @@ type Configuration struct { // Net specifies the net portion of the bind address. A default empty value means tcp. Net string `yaml:"net,omitempty"` + // Host specifies an externally-reachable address for the registry, as a fully + // qualified URL. + Host string `yaml:"host,omitempty"` + Prefix string `yaml:"prefix,omitempty"` // Secret specifies the secret key which HMAC tokens are created with. diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go index 58cdc1a9f..ced321a6e 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -65,6 +65,7 @@ var configStruct = Configuration{ HTTP: struct { Addr string `yaml:"addr,omitempty"` Net string `yaml:"net,omitempty"` + Host string `yaml:"host,omitempty"` Prefix string `yaml:"prefix,omitempty"` Secret string `yaml:"secret,omitempty"` TLS struct { diff --git a/docs/configuration.md b/docs/configuration.md index b4080cb07..55717c42e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -158,6 +158,7 @@ information about each option that appears later in this page. http: addr: localhost:5000 prefix: /my/nested/registry/ + host: https://myregistryaddress.org:5000 secret: asecretforlocaldevelopment tls: certificate: /path/to/x509/public @@ -1189,6 +1190,7 @@ configuration may contain both. addr: localhost:5000 net: tcp prefix: /my/nested/registry/ + host: https://myregistryaddress.org:5000 secret: asecretforlocaldevelopment tls: certificate: /path/to/x509/public @@ -1233,7 +1235,7 @@ The `http` option details the configuration for the HTTP server that hosts the r The default empty value means tcp. - + prefix @@ -1246,6 +1248,19 @@ prefix. The root path is the section before v2. It should have both preceding and trailing slashes, for example /path/. + + + host + + + no + + +This parameter specifies an externally-reachable address for the registry, as a +fully qualified URL. If present, it is used when creating generated URLs. +Otherwise, these URLs are derived from client requests. + + secret diff --git a/registry/api/v2/urls_test.go b/registry/api/v2/urls_test.go index 1113a7dde..61d415474 100644 --- a/registry/api/v2/urls_test.go +++ b/registry/api/v2/urls_test.go @@ -158,8 +158,9 @@ func TestBuilderFromRequest(t *testing.T) { forwardedHostHeader2.Set("X-Forwarded-Host", "first.example.com, proxy1.example.com") testRequests := []struct { - request *http.Request - base string + request *http.Request + base string + configHost url.URL }{ { request: &http.Request{URL: u, Host: u.Host}, @@ -177,10 +178,23 @@ func TestBuilderFromRequest(t *testing.T) { request: &http.Request{URL: u, Host: u.Host, Header: forwardedHostHeader2}, base: "http://first.example.com", }, + { + request: &http.Request{URL: u, Host: u.Host, Header: forwardedHostHeader2}, + base: "https://third.example.com:5000", + configHost: url.URL{ + Scheme: "https", + Host: "third.example.com:5000", + }, + }, } for _, tr := range testRequests { - builder := NewURLBuilderFromRequest(tr.request) + var builder *URLBuilder + if tr.configHost.Scheme != "" && tr.configHost.Host != "" { + builder = NewURLBuilder(&tr.configHost) + } else { + builder = NewURLBuilderFromRequest(tr.request) + } for _, testCase := range makeURLBuilderTestCases(builder) { url, err := testCase.build() @@ -207,8 +221,9 @@ func TestBuilderFromRequestWithPrefix(t *testing.T) { forwardedProtoHeader.Set("X-Forwarded-Proto", "https") testRequests := []struct { - request *http.Request - base string + request *http.Request + base string + configHost url.URL }{ { request: &http.Request{URL: u, Host: u.Host}, @@ -218,10 +233,23 @@ func TestBuilderFromRequestWithPrefix(t *testing.T) { request: &http.Request{URL: u, Host: u.Host, Header: forwardedProtoHeader}, base: "https://example.com/prefix/", }, + { + request: &http.Request{URL: u, Host: u.Host, Header: forwardedProtoHeader}, + base: "https://subdomain.example.com/prefix/", + configHost: url.URL{ + Scheme: "https", + Host: "subdomain.example.com/prefix", + }, + }, } for _, tr := range testRequests { - builder := NewURLBuilderFromRequest(tr.request) + var builder *URLBuilder + if tr.configHost.Scheme != "" && tr.configHost.Host != "" { + builder = NewURLBuilder(&tr.configHost) + } else { + builder = NewURLBuilderFromRequest(tr.request) + } for _, testCase := range makeURLBuilderTestCases(builder) { url, err := testCase.build() diff --git a/registry/handlers/app.go b/registry/handlers/app.go index 5103c5fbe..f2f6ad9d7 100644 --- a/registry/handlers/app.go +++ b/registry/handlers/app.go @@ -7,6 +7,7 @@ import ( "math/rand" "net" "net/http" + "net/url" "os" "time" @@ -54,6 +55,10 @@ type App struct { registry distribution.Namespace // registry is the primary registry backend for the app instance. accessController auth.AccessController // main access controller for application + // httpHost is a parsed representation of the http.host parameter from + // the configuration. Only the Scheme and Host fields are used. + httpHost url.URL + // events contains notification related configuration. events struct { sink notifications.Sink @@ -120,6 +125,14 @@ func NewApp(ctx context.Context, configuration *configuration.Configuration) *Ap app.configureRedis(configuration) app.configureLogHook(configuration) + if configuration.HTTP.Host != "" { + u, err := url.Parse(configuration.HTTP.Host) + if err != nil { + panic(fmt.Sprintf(`could not parse http "host" parameter: %v`, err)) + } + app.httpHost = *u + } + options := []storage.RegistryOption{} if app.isCache { @@ -639,9 +652,17 @@ func (app *App) context(w http.ResponseWriter, r *http.Request) *Context { "vars.uuid")) context := &Context{ - App: app, - Context: ctx, - urlBuilder: v2.NewURLBuilderFromRequest(r), + App: app, + Context: ctx, + } + + if app.httpHost.Scheme != "" && app.httpHost.Host != "" { + // A "host" item in the configuration takes precedence over + // X-Forwarded-Proto and X-Forwarded-Host headers, and the + // hostname in the request. + context.urlBuilder = v2.NewURLBuilder(&app.httpHost) + } else { + context.urlBuilder = v2.NewURLBuilderFromRequest(r) } return context