Merge pull request #846 from aaronlehmann/http-header-configuration

Add a section to the config file for HTTP headers to add to responses
This commit is contained in:
Richard Scothern 2015-08-18 12:53:05 -07:00
commit f169359798
8 changed files with 68 additions and 1 deletions

View file

@ -17,6 +17,8 @@ http:
secret: asecretforlocaldevelopment secret: asecretforlocaldevelopment
debug: debug:
addr: localhost:5001 addr: localhost:5001
headers:
X-Content-Type-Options: [nosniff]
redis: redis:
addr: localhost:6379 addr: localhost:6379
pool: pool:

View file

@ -32,6 +32,8 @@ http:
addr: :5000 addr: :5000
debug: debug:
addr: localhost:5001 addr: localhost:5001
headers:
X-Content-Type-Options: [nosniff]
redis: redis:
addr: localhost:6379 addr: localhost:6379
pool: pool:

View file

@ -9,3 +9,5 @@ storage:
rootdirectory: /var/lib/registry rootdirectory: /var/lib/registry
http: http:
addr: :5000 addr: :5000
headers:
X-Content-Type-Options: [nosniff]

View file

@ -86,6 +86,12 @@ type Configuration struct {
ClientCAs []string `yaml:"clientcas,omitempty"` ClientCAs []string `yaml:"clientcas,omitempty"`
} `yaml:"tls,omitempty"` } `yaml:"tls,omitempty"`
// Headers is a set of headers to include in HTTP responses. A common
// use case for this would be security headers such as
// Strict-Transport-Security. The map keys are the header names, and
// the values are the associated header payloads.
Headers http.Header `yaml:"headers,omitempty"`
// Debug configures the http debug interface, if specified. This can // Debug configures the http debug interface, if specified. This can
// include services such as pprof, expvar and other data that should // include services such as pprof, expvar and other data that should
// not be exposed externally. Left disabled by default. // not be exposed externally. Left disabled by default.

View file

@ -70,7 +70,8 @@ var configStruct = Configuration{
Key string `yaml:"key,omitempty"` Key string `yaml:"key,omitempty"`
ClientCAs []string `yaml:"clientcas,omitempty"` ClientCAs []string `yaml:"clientcas,omitempty"`
} `yaml:"tls,omitempty"` } `yaml:"tls,omitempty"`
Debug struct { Headers http.Header `yaml:"headers,omitempty"`
Debug struct {
Addr string `yaml:"addr,omitempty"` Addr string `yaml:"addr,omitempty"`
} `yaml:"debug,omitempty"` } `yaml:"debug,omitempty"`
}{ }{
@ -81,6 +82,9 @@ var configStruct = Configuration{
}{ }{
ClientCAs: []string{"/path/to/ca.pem"}, ClientCAs: []string{"/path/to/ca.pem"},
}, },
Headers: http.Header{
"X-Content-Type-Options": []string{"nosniff"},
},
}, },
} }
@ -118,6 +122,8 @@ reporting:
http: http:
clientcas: clientcas:
- /path/to/ca.pem - /path/to/ca.pem
headers:
X-Content-Type-Options: [nosniff]
` `
// inmemoryConfigYamlV0_1 is a Version 0.1 yaml document specifying an inmemory // inmemoryConfigYamlV0_1 is a Version 0.1 yaml document specifying an inmemory
@ -136,6 +142,9 @@ notifications:
url: http://example.com url: http://example.com
headers: headers:
Authorization: [Bearer <example>] Authorization: [Bearer <example>]
http:
headers:
X-Content-Type-Options: [nosniff]
` `
type ConfigSuite struct { type ConfigSuite struct {
@ -192,6 +201,7 @@ func (suite *ConfigSuite) TestParseIncomplete(c *C) {
suite.expectedConfig.Auth = Auth{"silly": Parameters{"realm": "silly"}} suite.expectedConfig.Auth = Auth{"silly": Parameters{"realm": "silly"}}
suite.expectedConfig.Reporting = Reporting{} suite.expectedConfig.Reporting = Reporting{}
suite.expectedConfig.Notifications = Notifications{} suite.expectedConfig.Notifications = Notifications{}
suite.expectedConfig.HTTP.Headers = nil
os.Setenv("REGISTRY_STORAGE", "filesystem") os.Setenv("REGISTRY_STORAGE", "filesystem")
os.Setenv("REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY", "/tmp/testroot") os.Setenv("REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY", "/tmp/testroot")
@ -366,5 +376,10 @@ func copyConfig(config Configuration) *Configuration {
configCopy.Notifications.Endpoints = append(configCopy.Notifications.Endpoints, v) configCopy.Notifications.Endpoints = append(configCopy.Notifications.Endpoints, v)
} }
configCopy.HTTP.Headers = make(http.Header)
for k, v := range config.HTTP.Headers {
configCopy.HTTP.Headers[k] = v
}
return configCopy return configCopy
} }

View file

@ -173,6 +173,8 @@ information about each option that appears later in this page.
- /path/to/another/ca.pem - /path/to/another/ca.pem
debug: debug:
addr: localhost:5001 addr: localhost:5001
headers:
X-Content-Type-Options: [nosniff]
notifications: notifications:
endpoints: endpoints:
- name: alistener - name: alistener
@ -1168,6 +1170,8 @@ configuration may contain both.
- /path/to/another/ca.pem - /path/to/another/ca.pem
debug: debug:
addr: localhost:5001 addr: localhost:5001
headers:
X-Content-Type-Options: [nosniff]
The `http` option details the configuration for the HTTP server that hosts the registry. The `http` option details the configuration for the HTTP server that hosts the registry.
@ -1296,6 +1300,21 @@ The `debug` section takes a single, required `addr` parameter. This parameter
specifies the `HOST:PORT` on which the debug server should accept connections. specifies the `HOST:PORT` on which the debug server should accept connections.
### headers
The `headers` option is **optional** . Use it to specify headers that the HTTP
server should include in responses. This can be used for security headers such
as `Strict-Transport-Security`.
The `headers` option should contain an option for each header to include, where
the parameter name is the header's name, and the parameter value a list of the
header's payload values.
Including `X-Content-Type-Options: [nosniff]` is recommended, so that browsers
will not interpret content as HTML if they are directed to load a page from the
registry. This header is included in the example configuration files.
## notifications ## notifications
notifications: notifications:

View file

@ -30,6 +30,10 @@ import (
"golang.org/x/net/context" "golang.org/x/net/context"
) )
var headerConfig = http.Header{
"X-Content-Type-Options": []string{"nosniff"},
}
// TestCheckAPI hits the base endpoint (/v2/) ensures we return the specified // TestCheckAPI hits the base endpoint (/v2/) ensures we return the specified
// 200 OK response. // 200 OK response.
func TestCheckAPI(t *testing.T) { func TestCheckAPI(t *testing.T) {
@ -215,6 +219,7 @@ func TestURLPrefix(t *testing.T) {
}, },
} }
config.HTTP.Prefix = "/test/" config.HTTP.Prefix = "/test/"
config.HTTP.Headers = headerConfig
env := newTestEnvWithConfig(t, &config) env := newTestEnvWithConfig(t, &config)
@ -1024,6 +1029,8 @@ func newTestEnv(t *testing.T, deleteEnabled bool) *testEnv {
}, },
} }
config.HTTP.Headers = headerConfig
return newTestEnvWithConfig(t, &config) return newTestEnvWithConfig(t, &config)
} }
@ -1240,6 +1247,14 @@ func checkResponse(t *testing.T, msg string, resp *http.Response, expectedStatus
t.FailNow() t.FailNow()
} }
// We expect the headers included in the configuration
if !reflect.DeepEqual(resp.Header["X-Content-Type-Options"], []string{"nosniff"}) {
t.Logf("missing or incorrect header X-Content-Type-Options %s", msg)
maybeDumpResponse(t, resp)
t.FailNow()
}
} }
// checkBodyHasErrorCodes ensures the body is an error body and has the // checkBodyHasErrorCodes ensures the body is an error body and has the

View file

@ -443,6 +443,12 @@ type dispatchFunc func(ctx *Context, r *http.Request) http.Handler
// handler, using the dispatch factory function. // handler, using the dispatch factory function.
func (app *App) dispatcher(dispatch dispatchFunc) http.Handler { func (app *App) dispatcher(dispatch dispatchFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for headerName, headerValues := range app.Config.HTTP.Headers {
for _, value := range headerValues {
w.Header().Add(headerName, value)
}
}
context := app.context(w, r) context := app.context(w, r)
if err := app.authorized(w, r, context); err != nil { if err := app.authorized(w, r, context); err != nil {