mirror of https://github.com/status-im/consul.git
add ability to specify response headers on the HTTP API
Add an config object that allows adding HTTP header response fields to every HTTP API response. Each specified header is added to every response from all HTTP API endpoints. Each individual endpoint may overwrite the specified header, which makes sure that Consul headers such as 'X-Consul-Index' is enforced by the API.
This commit is contained in:
parent
b74af612a9
commit
cb764c35e5
|
@ -310,6 +310,9 @@ type Config struct {
|
||||||
// send with the update check. This is used to deduplicate messages.
|
// send with the update check. This is used to deduplicate messages.
|
||||||
DisableAnonymousSignature bool `mapstructure:"disable_anonymous_signature"`
|
DisableAnonymousSignature bool `mapstructure:"disable_anonymous_signature"`
|
||||||
|
|
||||||
|
// HTTPAPIResponseHeaders are used to add HTTP header response fields to the HTTP API responses.
|
||||||
|
HTTPAPIResponseHeaders map[string]string `mapstructure:"http_api_response_headers"`
|
||||||
|
|
||||||
// AEInterval controls the anti-entropy interval. This is how often
|
// AEInterval controls the anti-entropy interval. This is how often
|
||||||
// the agent attempts to reconcile it's local state with the server'
|
// the agent attempts to reconcile it's local state with the server'
|
||||||
// representation of our state. Defaults to every 60s.
|
// representation of our state. Defaults to every 60s.
|
||||||
|
@ -859,6 +862,15 @@ func MergeConfig(a, b *Config) *Config {
|
||||||
result.DisableAnonymousSignature = true
|
result.DisableAnonymousSignature = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(b.HTTPAPIResponseHeaders) != 0 {
|
||||||
|
if result.HTTPAPIResponseHeaders == nil {
|
||||||
|
result.HTTPAPIResponseHeaders = make(map[string]string)
|
||||||
|
}
|
||||||
|
for field, value := range b.HTTPAPIResponseHeaders {
|
||||||
|
result.HTTPAPIResponseHeaders[field] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Copy the start join addresses
|
// Copy the start join addresses
|
||||||
result.StartJoin = make([]string, 0, len(a.StartJoin)+len(b.StartJoin))
|
result.StartJoin = make([]string, 0, len(a.StartJoin)+len(b.StartJoin))
|
||||||
result.StartJoin = append(result.StartJoin, a.StartJoin...)
|
result.StartJoin = append(result.StartJoin, a.StartJoin...)
|
||||||
|
|
|
@ -589,6 +589,21 @@ func TestDecodeConfig(t *testing.T) {
|
||||||
if !config.DisableAnonymousSignature {
|
if !config.DisableAnonymousSignature {
|
||||||
t.Fatalf("bad: %#v", config)
|
t.Fatalf("bad: %#v", config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTTP API response header fields
|
||||||
|
input = `{"http_api_response_headers": {"Access-Control-Allow-Origin": "*", "X-XSS-Protection": "1; mode=block"}}`
|
||||||
|
config, err = DecodeConfig(bytes.NewReader([]byte(input)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.HTTPAPIResponseHeaders["Access-Control-Allow-Origin"] != "*" {
|
||||||
|
t.Fatalf("bad: %#v", config)
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.HTTPAPIResponseHeaders["X-XSS-Protection"] != "1; mode=block" {
|
||||||
|
t.Fatalf("bad: %#v", config)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDecodeConfig_Services(t *testing.T) {
|
func TestDecodeConfig_Services(t *testing.T) {
|
||||||
|
@ -960,6 +975,9 @@ func TestMergeConfig(t *testing.T) {
|
||||||
StatsdAddr: "127.0.0.1:7251",
|
StatsdAddr: "127.0.0.1:7251",
|
||||||
DisableUpdateCheck: true,
|
DisableUpdateCheck: true,
|
||||||
DisableAnonymousSignature: true,
|
DisableAnonymousSignature: true,
|
||||||
|
HTTPAPIResponseHeaders: map[string]string{
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
c := MergeConfig(a, b)
|
c := MergeConfig(a, b)
|
||||||
|
|
|
@ -231,6 +231,8 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) {
|
||||||
// wrap is used to wrap functions to make them more convenient
|
// wrap is used to wrap functions to make them more convenient
|
||||||
func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Request) (interface{}, error)) func(resp http.ResponseWriter, req *http.Request) {
|
func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Request) (interface{}, error)) func(resp http.ResponseWriter, req *http.Request) {
|
||||||
f := func(resp http.ResponseWriter, req *http.Request) {
|
f := func(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
setHeaders(resp, s.agent.config.HTTPAPIResponseHeaders)
|
||||||
|
|
||||||
// Invoke the handler
|
// Invoke the handler
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -336,6 +338,13 @@ func setMeta(resp http.ResponseWriter, m *structs.QueryMeta) {
|
||||||
setKnownLeader(resp, m.KnownLeader)
|
setKnownLeader(resp, m.KnownLeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setHeaders is used to set canonical response header fields
|
||||||
|
func setHeaders(resp http.ResponseWriter, headers map[string]string) {
|
||||||
|
for field, value := range headers {
|
||||||
|
resp.Header().Set(http.CanonicalHeaderKey(field), value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// parseWait is used to parse the ?wait and ?index query params
|
// parseWait is used to parse the ?wait and ?index query params
|
||||||
// Returns true on error
|
// Returns true on error
|
||||||
func parseWait(resp http.ResponseWriter, req *http.Request, b *structs.QueryOptions) bool {
|
func parseWait(resp http.ResponseWriter, req *http.Request, b *structs.QueryOptions) bool {
|
||||||
|
|
|
@ -102,6 +102,37 @@ func TestSetMeta(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHTTPAPIResponseHeaders(t *testing.T) {
|
||||||
|
dir, srv := makeHTTPServer(t)
|
||||||
|
srv.agent.config.HTTPAPIResponseHeaders = map[string]string{
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"X-XSS-Protection": "1; mode=block",
|
||||||
|
}
|
||||||
|
|
||||||
|
defer os.RemoveAll(dir)
|
||||||
|
defer srv.Shutdown()
|
||||||
|
defer srv.agent.Shutdown()
|
||||||
|
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("GET", "/v1/agent/self", nil)
|
||||||
|
srv.wrap(handler)(resp, req)
|
||||||
|
|
||||||
|
origin := resp.Header().Get("Access-Control-Allow-Origin")
|
||||||
|
if origin != "*" {
|
||||||
|
t.Fatalf("bad Access-Control-Allow-Origin: expected %q, got %q", "*", origin)
|
||||||
|
}
|
||||||
|
|
||||||
|
xss := resp.Header().Get("X-XSS-Protection")
|
||||||
|
if xss != "1; mode=block" {
|
||||||
|
t.Fatalf("bad X-XSS-Protection header: expected %q, got %q", "1; mode=block", xss)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestContentTypeIsJSON(t *testing.T) {
|
func TestContentTypeIsJSON(t *testing.T) {
|
||||||
dir, srv := makeHTTPServer(t)
|
dir, srv := makeHTTPServer(t)
|
||||||
|
|
||||||
|
|
|
@ -5,5 +5,8 @@
|
||||||
"bootstrap": true,
|
"bootstrap": true,
|
||||||
"server": true,
|
"server": true,
|
||||||
"acl_datacenter": "dc1",
|
"acl_datacenter": "dc1",
|
||||||
"acl_master_token": "dev"
|
"acl_master_token": "dev",
|
||||||
|
"http_api_response_headers": {
|
||||||
|
"Access-Control-Allow-Origin": "*"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -172,7 +172,7 @@ being configured using a configuration management system.
|
||||||
|
|
||||||
The configuration files are JSON formatted, making them easily readable
|
The configuration files are JSON formatted, making them easily readable
|
||||||
and editable by both humans and computers. The configuration is formatted
|
and editable by both humans and computers. The configuration is formatted
|
||||||
at a single JSON object with configuration within it.
|
as a single JSON object with configuration within it.
|
||||||
|
|
||||||
Configuration files are used for more than just setting up the agent,
|
Configuration files are used for more than just setting up the agent,
|
||||||
they are also used to provide check and service definitions. These are used
|
they are also used to provide check and service definitions. These are used
|
||||||
|
@ -327,6 +327,18 @@ definitions support being updated during a reload.
|
||||||
The key is used with the certificate to verify the agents authenticity.
|
The key is used with the certificate to verify the agents authenticity.
|
||||||
Must be provided along with the `cert_file`.
|
Must be provided along with the `cert_file`.
|
||||||
|
|
||||||
|
* `http_api_response_headers` - This object allows adding HTTP header response fields to
|
||||||
|
the HTTP API responses. For example, the following config can be used to enable CORS on
|
||||||
|
the HTTP API endpoints:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"http_api_response_headers": {
|
||||||
|
"Access-Control-Allow-Origin": "*"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
* `leave_on_terminate` - If enabled, when the agent receives a TERM signal,
|
* `leave_on_terminate` - If enabled, when the agent receives a TERM signal,
|
||||||
it will send a Leave message to the rest of the cluster and gracefully
|
it will send a Leave message to the rest of the cluster and gracefully
|
||||||
leave. Defaults to false.
|
leave. Defaults to false.
|
||||||
|
|
Loading…
Reference in New Issue