Merge pull request #558 from ceh/http-api-response-headers
add ability to specify response headers on the HTTP API
This commit is contained in:
commit
1088a5c170
|
@ -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