Skip to content

Commit df16a71

Browse files
[query] Support additional/custom HTTP headers (#2056)
* Added new FlagList type and used it in jaeger-query Signed-off-by: Joe Elliott <[email protected]> * Added tests to confirm FlagList will be treated as stringSlice by viper Signed-off-by: Joe Elliott <[email protected]> * Added additionalheaders logic. Added tests Signed-off-by: Joe Elliott <[email protected]> * Changed http handler to take a header struct Signed-off-by: Joe Elliott <[email protected]> * Updated help text on param Signed-off-by: Joe Elliott <[email protected]> * FlagList => StringSlice Signed-off-by: Joe Elliott <[email protected]> * Additional tests to further specify string slice behavior Signed-off-by: Joe Elliott <[email protected]> * Added header parsing method and tests Signed-off-by: Joe Elliott <[email protected]> * Added support for commas in params Signed-off-by: Joe Elliott <[email protected]> * Updated flags to use expected format Signed-off-by: Joe Elliott <[email protected]> * Moved stringSliceAsHeader() to flags Signed-off-by: Joe Elliott <[email protected]> * Added test for empty string slice Signed-off-by: Joe Elliott <[email protected]> * Moved additional headers to server handler Signed-off-by: Joe Elliott <[email protected]> * Added handler and tests Signed-off-by: Joe Elliott <[email protected]> * removed unnecessary whitespace Signed-off-by: Joe Elliott <[email protected]> * checked out http_handler_test to master Signed-off-by: Joe Elliott <[email protected]> * checked out http_handler to master Signed-off-by: Joe Elliott <[email protected]> * Update cmd/query/app/additional_headers_test.go Co-Authored-By: Yuri Shkuro <[email protected]> Signed-off-by: Joe Elliott <[email protected]> * Update cmd/query/app/flags_test.go Co-Authored-By: Yuri Shkuro <[email protected]> Signed-off-by: Joe Elliott <[email protected]> * Update cmd/query/app/flags_test.go Co-Authored-By: Yuri Shkuro <[email protected]> Signed-off-by: Joe Elliott <[email protected]> * Made stringSliceAsHeader return err Signed-off-by: Joe Elliott <[email protected]> * Added test for bad header params Signed-off-by: Joe Elliott <[email protected]> Co-authored-by: Yuri Shkuro <[email protected]>
1 parent 29e7131 commit df16a71

9 files changed

+280
-11
lines changed

Diff for: cmd/all-in-one/main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ by default uses only in-memory database.`,
129129
tchanBuilder := agentTchanRep.NewBuilder().InitFromViper(v, logger)
130130
grpcBuilder := agentGrpcRep.NewConnBuilder().InitFromViper(v)
131131
cOpts := new(collector.CollectorOptions).InitFromViper(v)
132-
qOpts := new(queryApp.QueryOptions).InitFromViper(v)
132+
qOpts := new(queryApp.QueryOptions).InitFromViper(v, logger)
133133

134134
collectorSrv := startCollector(cOpts, spanWriter, logger, metricsFactory, strategyStore, svc.HC())
135135
startAgent(aOpts, repOpts, tchanBuilder, grpcBuilder, cOpts, logger, metricsFactory)

Diff for: cmd/query/app/additional_headers_handler.go

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) 2020 The Jaeger Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package app
16+
17+
import (
18+
"net/http"
19+
)
20+
21+
func additionalHeadersHandler(h http.Handler, additionalHeaders http.Header) http.Handler {
22+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
23+
header := w.Header()
24+
for key, values := range additionalHeaders {
25+
header[key] = values
26+
}
27+
28+
h.ServeHTTP(w, r)
29+
})
30+
}

Diff for: cmd/query/app/additional_headers_test.go

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright (c) 2020 The Jaeger Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package app
16+
17+
import (
18+
"net/http"
19+
"net/http/httptest"
20+
"testing"
21+
22+
"github.com/stretchr/testify/assert"
23+
)
24+
25+
func TestAdditionalHeadersHandler(t *testing.T) {
26+
additionalHeaders := http.Header{}
27+
additionalHeaders.Add("Access-Control-Allow-Origin", "https://mozilla.org")
28+
additionalHeaders.Add("Access-Control-Expose-Headers", "X-My-Custom-Header")
29+
additionalHeaders.Add("Access-Control-Expose-Headers", "X-Another-Custom-Header")
30+
additionalHeaders.Add("Access-Control-Request-Headers", "field1, field2")
31+
32+
emptyHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
33+
w.Write([]byte{})
34+
})
35+
36+
handler := additionalHeadersHandler(emptyHandler, additionalHeaders)
37+
server := httptest.NewServer(handler)
38+
defer server.Close()
39+
40+
req, err := http.NewRequest("GET", server.URL, nil)
41+
assert.NoError(t, err)
42+
43+
resp, err := server.Client().Do(req)
44+
assert.NoError(t, err)
45+
46+
for k, v := range additionalHeaders {
47+
assert.Equal(t, v, resp.Header[k])
48+
}
49+
}

Diff for: cmd/query/app/flags.go

+46-7
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,28 @@
1616
package app
1717

1818
import (
19+
"bufio"
1920
"flag"
21+
"fmt"
22+
"io"
23+
"net/http"
24+
"net/textproto"
25+
"strings"
2026

2127
"github.com/spf13/viper"
28+
"go.uber.org/zap"
2229

30+
"github.com/jaegertracing/jaeger/pkg/config"
2331
"github.com/jaegertracing/jaeger/ports"
2432
)
2533

2634
const (
27-
queryPort = "query.port"
28-
queryBasePath = "query.base-path"
29-
queryStaticFiles = "query.static-files"
30-
queryUIConfig = "query.ui-config"
31-
queryTokenPropagation = "query.bearer-token-propagation"
35+
queryPort = "query.port"
36+
queryBasePath = "query.base-path"
37+
queryStaticFiles = "query.static-files"
38+
queryUIConfig = "query.ui-config"
39+
queryTokenPropagation = "query.bearer-token-propagation"
40+
queryAdditionalHeaders = "query.additional-headers"
3241
)
3342

3443
// QueryOptions holds configuration for query service
@@ -43,24 +52,54 @@ type QueryOptions struct {
4352
UIConfig string
4453
// BearerTokenPropagation activate/deactivate bearer token propagation to storage
4554
BearerTokenPropagation bool
55+
// AdditionalHeaders
56+
AdditionalHeaders http.Header
4657
}
4758

4859
// AddFlags adds flags for QueryOptions
4960
func AddFlags(flagSet *flag.FlagSet) {
61+
flagSet.Var(&config.StringSlice{}, queryAdditionalHeaders, `Additional HTTP response headers. Can be specified multiple times. Format: "Key: Value"`)
5062
flagSet.Int(queryPort, ports.QueryHTTP, "The port for the query service")
5163
flagSet.String(queryBasePath, "/", "The base path for all HTTP routes, e.g. /jaeger; useful when running behind a reverse proxy")
5264
flagSet.String(queryStaticFiles, "", "The directory path override for the static assets for the UI")
5365
flagSet.String(queryUIConfig, "", "The path to the UI configuration file in JSON format")
5466
flagSet.Bool(queryTokenPropagation, false, "Allow propagation of bearer token to be used by storage plugins")
55-
5667
}
5768

5869
// InitFromViper initializes QueryOptions with properties from viper
59-
func (qOpts *QueryOptions) InitFromViper(v *viper.Viper) *QueryOptions {
70+
func (qOpts *QueryOptions) InitFromViper(v *viper.Viper, logger *zap.Logger) *QueryOptions {
6071
qOpts.Port = v.GetInt(queryPort)
6172
qOpts.BasePath = v.GetString(queryBasePath)
6273
qOpts.StaticAssets = v.GetString(queryStaticFiles)
6374
qOpts.UIConfig = v.GetString(queryUIConfig)
6475
qOpts.BearerTokenPropagation = v.GetBool(queryTokenPropagation)
76+
77+
stringSlice := v.GetStringSlice(queryAdditionalHeaders)
78+
headers, err := stringSliceAsHeader(stringSlice)
79+
if err != nil {
80+
logger.Error("Failed to parse headers", zap.Strings("slice", stringSlice), zap.Error(err))
81+
} else {
82+
qOpts.AdditionalHeaders = headers
83+
}
6584
return qOpts
6685
}
86+
87+
// stringSliceAsHeader parses a slice of strings and returns a http.Header.
88+
// Each string in the slice is expected to be in the format "key: value"
89+
func stringSliceAsHeader(slice []string) (http.Header, error) {
90+
if len(slice) == 0 {
91+
return nil, nil
92+
}
93+
94+
allHeaders := strings.Join(slice, "\r\n")
95+
96+
reader := bufio.NewReader(strings.NewReader(allHeaders))
97+
tp := textproto.NewReader(reader)
98+
99+
header, err := tp.ReadMIMEHeader()
100+
if err != nil && err != io.EOF {
101+
return nil, fmt.Errorf("failed to parse headers")
102+
}
103+
104+
return http.Header(header), nil
105+
}

Diff for: cmd/query/app/flags_test.go

+45-1
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
package app
1717

1818
import (
19+
"net/http"
1920
"testing"
2021

2122
"github.com/stretchr/testify/assert"
23+
"go.uber.org/zap"
2224

2325
"github.com/jaegertracing/jaeger/pkg/config"
2426
)
@@ -30,10 +32,52 @@ func TestQueryBuilderFlags(t *testing.T) {
3032
"--query.ui-config=some.json",
3133
"--query.base-path=/jaeger",
3234
"--query.port=80",
35+
"--query.additional-headers=access-control-allow-origin:blerg",
36+
"--query.additional-headers=whatever:thing",
3337
})
34-
qOpts := new(QueryOptions).InitFromViper(v)
38+
qOpts := new(QueryOptions).InitFromViper(v, zap.NewNop())
3539
assert.Equal(t, "/dev/null", qOpts.StaticAssets)
3640
assert.Equal(t, "some.json", qOpts.UIConfig)
3741
assert.Equal(t, "/jaeger", qOpts.BasePath)
3842
assert.Equal(t, 80, qOpts.Port)
43+
assert.Equal(t, http.Header{
44+
"Access-Control-Allow-Origin": []string{"blerg"},
45+
"Whatever": []string{"thing"},
46+
}, qOpts.AdditionalHeaders)
47+
}
48+
49+
func TestQueryBuilderBadHeadersFlags(t *testing.T) {
50+
v, command := config.Viperize(AddFlags)
51+
command.ParseFlags([]string{
52+
"--query.additional-headers=malformedheader",
53+
})
54+
qOpts := new(QueryOptions).InitFromViper(v, zap.NewNop())
55+
assert.Nil(t, qOpts.AdditionalHeaders)
56+
}
57+
58+
func TestStringSliceAsHeader(t *testing.T) {
59+
headers := []string{
60+
"Access-Control-Allow-Origin: https://mozilla.org",
61+
"Access-Control-Expose-Headers: X-My-Custom-Header",
62+
"Access-Control-Expose-Headers: X-Another-Custom-Header",
63+
}
64+
65+
parsedHeaders, err := stringSliceAsHeader(headers)
66+
67+
assert.Equal(t, []string{"https://mozilla.org"}, parsedHeaders["Access-Control-Allow-Origin"])
68+
assert.Equal(t, []string{"X-My-Custom-Header", "X-Another-Custom-Header"}, parsedHeaders["Access-Control-Expose-Headers"])
69+
assert.NoError(t, err)
70+
71+
malformedHeaders := append(headers, "this is not a valid header")
72+
parsedHeaders, err = stringSliceAsHeader(malformedHeaders)
73+
assert.Nil(t, parsedHeaders)
74+
assert.Error(t, err)
75+
76+
parsedHeaders, err = stringSliceAsHeader([]string{})
77+
assert.Nil(t, parsedHeaders)
78+
assert.NoError(t, err)
79+
80+
parsedHeaders, err = stringSliceAsHeader(nil)
81+
assert.Nil(t, parsedHeaders)
82+
assert.NoError(t, err)
3983
}

Diff for: cmd/query/app/server.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,9 @@ func createHTTPServer(querySvc *querysvc.QueryService, queryOpts *QueryOptions,
8282
apiHandler.RegisterRoutes(r)
8383
RegisterStaticHandler(r, logger, queryOpts)
8484
var handler http.Handler = r
85+
handler = additionalHeadersHandler(handler, queryOpts.AdditionalHeaders)
8586
if queryOpts.BearerTokenPropagation {
86-
handler = bearerTokenPropagationHandler(logger, r)
87+
handler = bearerTokenPropagationHandler(logger, handler)
8788
}
8889
handler = handlers.CompressHandler(handler)
8990
recoveryHandler := recoveryhandler.NewRecoveryHandler(logger, true)

Diff for: cmd/query/main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func main() {
8585
}
8686
defer closer.Close()
8787
opentracing.SetGlobalTracer(tracer)
88-
queryOpts := new(app.QueryOptions).InitFromViper(v)
88+
queryOpts := new(app.QueryOptions).InitFromViper(v, logger)
8989
// TODO: Need to figure out set enable/disable propagation on storage plugins.
9090
v.Set(spanstore.StoragePropagationKey, queryOpts.BearerTokenPropagation)
9191
storageFactory.InitFromViper(v)

Diff for: pkg/config/string_slice.go

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright (c) 2020 The Jaeger Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package config
16+
17+
import "strings"
18+
19+
// StringSlice implements the pflag.Value interface and allows for parsing multiple
20+
// config values with the same name
21+
// It purposefully mimics pFlag.stringSliceValue (https://github.com/spf13/pflag/blob/master/string_slice.go)
22+
// in order to be treated like a string slice by both viper and pflag cleanly
23+
type StringSlice []string
24+
25+
// String implements pflag.Value
26+
func (l *StringSlice) String() string {
27+
if len(*l) == 0 {
28+
return "[]"
29+
}
30+
31+
return `["` + strings.Join(*l, `","`) + `"]`
32+
}
33+
34+
// Set implements pflag.Value
35+
func (l *StringSlice) Set(value string) error {
36+
*l = append(*l, value)
37+
return nil
38+
}
39+
40+
// Type implements pflag.Value
41+
func (l *StringSlice) Type() string {
42+
// this type string needs to match pflag.stringSliceValue's Type
43+
return "stringSlice"
44+
}

Diff for: pkg/config/string_slice_test.go

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) 2020 The Jaeger Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package config
16+
17+
import (
18+
"flag"
19+
"testing"
20+
21+
"github.com/spf13/pflag"
22+
"github.com/stretchr/testify/assert"
23+
)
24+
25+
func TestStringSlice(t *testing.T) {
26+
f := &StringSlice{}
27+
28+
assert.Equal(t, "[]", f.String())
29+
assert.Equal(t, "stringSlice", f.Type())
30+
31+
f.Set("test")
32+
assert.Equal(t, `["test"]`, f.String())
33+
34+
f.Set("test2")
35+
assert.Equal(t, `["test","test2"]`, f.String())
36+
37+
f.Set("test3,test4")
38+
assert.Equal(t, `["test","test2","test3,test4"]`, f.String())
39+
}
40+
41+
func TestStringSliceTreatedAsStringSlice(t *testing.T) {
42+
f := &StringSlice{}
43+
44+
// create and add flags/values to a go flag set
45+
flagset := flag.NewFlagSet("test", flag.ContinueOnError)
46+
flagset.Var(f, "test", "test")
47+
48+
err := flagset.Set("test", "asdf")
49+
assert.NoError(t, err)
50+
err = flagset.Set("test", "blerg")
51+
assert.NoError(t, err)
52+
err = flagset.Set("test", "other,thing")
53+
assert.NoError(t, err)
54+
55+
// add go flag set to pflag
56+
pflagset := pflag.FlagSet{}
57+
pflagset.AddGoFlagSet(flagset)
58+
actual, err := pflagset.GetStringSlice("test")
59+
assert.NoError(t, err)
60+
61+
assert.Equal(t, []string{"asdf", "blerg", "other,thing"}, actual)
62+
}

0 commit comments

Comments
 (0)