Merge pull request #846 from aaronlehmann/http-header-configuration
Add a section to the config file for HTTP headers to add to responsesmaster
						commit
						f169359798
					
				|  | @ -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: | ||||||
|  |  | ||||||
|  | @ -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: | ||||||
|  |  | ||||||
|  | @ -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] | ||||||
|  |  | ||||||
|  | @ -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.
 | ||||||
|  |  | ||||||
|  | @ -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 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -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: | ||||||
|  |  | ||||||
|  | @ -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
 | ||||||
|  |  | ||||||
|  | @ -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 { | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue