Skip to content

Commit

Permalink
add ability to specify response headers on the HTTP API
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
ceh committed Dec 28, 2014
1 parent b74af61 commit cb764c3
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 2 deletions.
12 changes: 12 additions & 0 deletions command/agent/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,9 @@ type Config struct {
// send with the update check. This is used to deduplicate messages.
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
// the agent attempts to reconcile it's local state with the server'
// representation of our state. Defaults to every 60s.
Expand Down Expand Up @@ -859,6 +862,15 @@ func MergeConfig(a, b *Config) *Config {
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
result.StartJoin = make([]string, 0, len(a.StartJoin)+len(b.StartJoin))
result.StartJoin = append(result.StartJoin, a.StartJoin...)
Expand Down
18 changes: 18 additions & 0 deletions command/agent/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,21 @@ func TestDecodeConfig(t *testing.T) {
if !config.DisableAnonymousSignature {
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) {
Expand Down Expand Up @@ -960,6 +975,9 @@ func TestMergeConfig(t *testing.T) {
StatsdAddr: "127.0.0.1:7251",
DisableUpdateCheck: true,
DisableAnonymousSignature: true,
HTTPAPIResponseHeaders: map[string]string{
"Access-Control-Allow-Origin": "*",
},
}

c := MergeConfig(a, b)
Expand Down
9 changes: 9 additions & 0 deletions command/agent/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,8 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) {
// 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) {
f := func(resp http.ResponseWriter, req *http.Request) {
setHeaders(resp, s.agent.config.HTTPAPIResponseHeaders)

// Invoke the handler
start := time.Now()
defer func() {
Expand Down Expand Up @@ -336,6 +338,13 @@ func setMeta(resp http.ResponseWriter, m *structs.QueryMeta) {
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
// Returns true on error
func parseWait(resp http.ResponseWriter, req *http.Request, b *structs.QueryOptions) bool {
Expand Down
31 changes: 31 additions & 0 deletions command/agent/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
dir, srv := makeHTTPServer(t)

Expand Down
5 changes: 4 additions & 1 deletion ui/development_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@
"bootstrap": true,
"server": true,
"acl_datacenter": "dc1",
"acl_master_token": "dev"
"acl_master_token": "dev",
"http_api_response_headers": {
"Access-Control-Allow-Origin": "*"
}
}
14 changes: 13 additions & 1 deletion website/source/docs/agent/options.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ being configured using a configuration management system.

The configuration files are JSON formatted, making them easily readable
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,
they are also used to provide check and service definitions. These are used
Expand Down Expand Up @@ -327,6 +327,18 @@ definitions support being updated during a reload.
The key is used with the certificate to verify the agents authenticity.
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,
it will send a Leave message to the rest of the cluster and gracefully
leave. Defaults to false.
Expand Down

0 comments on commit cb764c3

Please sign in to comment.