Skip to content

Commit

Permalink
Add headers to HTTPServerSettings (#7697)
Browse files Browse the repository at this point in the history
Add `headers` configuration option on HTTPServerSettings. It allows for
additional headers to be attached to each HTTP response sent to the
client
  • Loading branch information
ledor473 authored Jun 21, 2023
1 parent c6b5e77 commit 58628a7
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 2 deletions.
16 changes: 16 additions & 0 deletions .chloggen/http-server-headers-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement

# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
component: confighttp

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Add `response_headers` configuration option on HTTPServerSettings. It allows for additional headers to be attached to each HTTP response sent to the client

# One or more tracking issues or pull requests related to the change
issues: [7328]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:
22 changes: 21 additions & 1 deletion config/confighttp/confighttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,10 @@ type HTTPServerSettings struct {
// IncludeMetadata propagates the client metadata from the incoming requests to the downstream consumers
// Experimental: *NOTE* this option is subject to change or removal in the future.
IncludeMetadata bool `mapstructure:"include_metadata"`

// Additional headers attached to each HTTP response sent to the client.
// Header values are opaque since they may be sensitive.
ResponseHeaders map[string]configopaque.String `mapstructure:"response_headers"`
}

// ToListener creates a net.Listener.
Expand Down Expand Up @@ -285,6 +289,7 @@ func (hss *HTTPServerSettings) ToServer(host component.Host, settings component.
handler = authInterceptor(handler, server)
}

// TODO: emit a warning when non-empty CorsHeaders and empty CorsOrigins.
if hss.CORS != nil && len(hss.CORS.AllowedOrigins) > 0 {
co := cors.Options{
AllowedOrigins: hss.CORS.AllowedOrigins,
Expand All @@ -294,7 +299,10 @@ func (hss *HTTPServerSettings) ToServer(host component.Host, settings component.
}
handler = cors.New(co).Handler(handler)
}
// TODO: emit a warning when non-empty CorsHeaders and empty CorsOrigins.

if hss.ResponseHeaders != nil {
handler = responseHeadersHandler(handler, hss.ResponseHeaders)
}

// Enable OpenTelemetry observability plugin.
// TODO: Consider to use component ID string as prefix for all the operations.
Expand All @@ -320,6 +328,18 @@ func (hss *HTTPServerSettings) ToServer(host component.Host, settings component.
}, nil
}

func responseHeadersHandler(handler http.Handler, headers map[string]configopaque.String) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := w.Header()

for k, v := range headers {
h.Set(k, string(v))
}

handler.ServeHTTP(w, r)
})
}

// CORSSettings configures a receiver for HTTP cross-origin resource sharing (CORS).
// See the underlying https://github.com/rs/cors package for details.
type CORSSettings struct {
Expand Down
79 changes: 78 additions & 1 deletion config/confighttp/confighttp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,64 @@ func TestHttpCorsWithSettings(t *testing.T) {
assert.Equal(t, "*", rec.Header().Get("Access-Control-Allow-Origin"))
}

func TestHttpServerHeaders(t *testing.T) {
tests := []struct {
name string
headers map[string]configopaque.String
}{
{
name: "noHeaders",
headers: nil,
},
{
name: "emptyHeaders",
headers: map[string]configopaque.String{},
},
{
name: "withHeaders",
headers: map[string]configopaque.String{
"x-new-header-1": "value1",
"x-new-header-2": "value2",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hss := &HTTPServerSettings{
Endpoint: "localhost:0",
ResponseHeaders: tt.headers,
}

ln, err := hss.ToListener()
require.NoError(t, err)

s, err := hss.ToServer(
componenttest.NewNopHost(),
componenttest.NewNopTelemetrySettings(),
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
require.NoError(t, err)

go func() {
_ = s.Serve(ln)
}()

// TODO: make starting server deterministic
// Wait for the servers to start
<-time.After(10 * time.Millisecond)

url := fmt.Sprintf("http://%s", ln.Addr().String())

// Verify allowed domain gets responses that allow CORS.
verifyHeadersResp(t, url, tt.headers)

require.NoError(t, s.Close())
})
}
}

func verifyCorsResp(t *testing.T, url string, origin string, maxAge int, extraHeader bool, wantStatus int, wantAllowed bool) {
req, err := http.NewRequest(http.MethodOptions, url, nil)
require.NoError(t, err, "Error creating trace OPTIONS request: %v", err)
Expand Down Expand Up @@ -839,6 +897,25 @@ func verifyCorsResp(t *testing.T, url string, origin string, maxAge int, extraHe
assert.Equal(t, wantMaxAge, resp.Header.Get("Access-Control-Max-Age"))
}

func verifyHeadersResp(t *testing.T, url string, expected map[string]configopaque.String) {
req, err := http.NewRequest(http.MethodGet, url, nil)
require.NoError(t, err, "Error creating request: %v", err)

resp, err := http.DefaultClient.Do(req)
require.NoError(t, err, "Error sending request to http server: %v", err)

err = resp.Body.Close()
if err != nil {
t.Errorf("Error closing response body, %v", err)
}

assert.Equal(t, http.StatusOK, resp.StatusCode)

for k, v := range expected {
assert.Equal(t, string(v), resp.Header.Get(k))
}
}

func ExampleHTTPServerSettings() {
settings := HTTPServerSettings{
Endpoint: "localhost:443",
Expand All @@ -860,7 +937,7 @@ func ExampleHTTPServerSettings() {
}
}

func TestHttpHeaders(t *testing.T) {
func TestHttpClientHeaders(t *testing.T) {
tests := []struct {
name string
headers map[string]configopaque.String
Expand Down

0 comments on commit 58628a7

Please sign in to comment.