Skip to content

Commit 89ced70

Browse files
authored
Pull most configurable functions into plugins (#22)
1 parent 1a25823 commit 89ced70

File tree

16 files changed

+706
-244
lines changed

16 files changed

+706
-244
lines changed

relay/config.go

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ package relay
33
import (
44
"fmt"
55
"net/url"
6-
"regexp"
76
"strconv"
8-
"strings"
97

108
"github.com/fullstorydev/relay-core/relay/commands"
119
"github.com/fullstorydev/relay-core/relay/traffic"
@@ -36,6 +34,8 @@ func ReadConfig(env *commands.Environment) (*Config, error) {
3634
if err := env.ParseRequired("TRAFFIC_RELAY_TARGET", func(key string, value string) error {
3735
if targetURL, err := url.Parse(value); err != nil {
3836
return err
37+
} else if targetURL.Scheme == "" || targetURL.Host == "" {
38+
return fmt.Errorf("Invalid or relative target URL")
3939
} else {
4040
config.Relay.TargetScheme = targetURL.Scheme
4141
config.Relay.TargetHost = targetURL.Host
@@ -45,12 +45,6 @@ func ReadConfig(env *commands.Environment) (*Config, error) {
4545
return nil, err
4646
}
4747

48-
if cookiesVar, ok := env.LookupOptional("TRAFFIC_RELAY_COOKIES"); ok {
49-
for _, cookieName := range strings.Split(cookiesVar, " ") { // Should we support spaces?
50-
config.Relay.RelayedCookies[cookieName] = true
51-
}
52-
}
53-
5448
if err := env.ParseOptional("TRAFFIC_RELAY_MAX_BODY_SIZE", func(key string, value string) error {
5549
if maxBodySize, err := strconv.ParseInt(value, 10, 64); err != nil {
5650
return err
@@ -62,35 +56,5 @@ func ReadConfig(env *commands.Environment) (*Config, error) {
6256
return nil, err
6357
}
6458

65-
if originOverrideVar, ok := env.LookupOptional("TRAFFIC_RELAY_ORIGIN_OVERRIDE"); ok {
66-
config.Relay.OriginOverride = originOverrideVar
67-
}
68-
69-
if err := env.ParseOptional("TRAFFIC_RELAY_SPECIALS", func(key string, value string) error {
70-
specialsTokens := strings.Split(value, " ")
71-
if len(specialsTokens)%2 != 0 {
72-
return fmt.Errorf("Last key has no value")
73-
}
74-
75-
for i := 0; i < len(specialsTokens); i += 2 {
76-
matchVar := specialsTokens[i]
77-
replacementString := specialsTokens[i+1]
78-
matchRE, err := regexp.Compile(matchVar)
79-
if err != nil {
80-
return fmt.Errorf("Could not compile regular expression \"%v\": %v", matchVar, err)
81-
}
82-
special := traffic.SpecialPath{
83-
Match: matchRE,
84-
Replacement: replacementString,
85-
}
86-
config.Relay.SpecialPaths = append(config.Relay.SpecialPaths, special)
87-
logger.Printf("Relaying special expression \"%v\" to \"%v\"", special.Match, special.Replacement)
88-
}
89-
90-
return nil
91-
}); err != nil {
92-
return nil, err
93-
}
94-
9559
return config, nil
9660
}

relay/main/main.go

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,11 @@ import (
77

88
"github.com/fullstorydev/relay-core/relay"
99
"github.com/fullstorydev/relay-core/relay/commands"
10-
"github.com/fullstorydev/relay-core/relay/plugins/traffic/content-blocker-plugin"
11-
"github.com/fullstorydev/relay-core/relay/plugins/traffic/paths-plugin"
12-
"github.com/fullstorydev/relay-core/relay/traffic"
10+
"github.com/fullstorydev/relay-core/relay/traffic/plugin-loader"
1311
)
1412

1513
var logger = log.New(os.Stdout, "[relay] ", 0)
1614

17-
// The default set of traffic plugins.
18-
var DefaultPlugins = []traffic.PluginFactory{
19-
paths_plugin.Factory,
20-
content_blocker_plugin.Factory,
21-
}
22-
2315
func main() {
2416
envProvider := commands.NewDefaultEnvironmentProvider()
2517
env := commands.NewEnvironment(envProvider)
@@ -29,7 +21,7 @@ func main() {
2921
os.Exit(1)
3022
}
3123

32-
trafficPlugins, err := traffic.LoadPlugins(DefaultPlugins, env)
24+
trafficPlugins, err := plugin_loader.Load(plugin_loader.DefaultPlugins, env)
3325
if err != nil {
3426
logger.Println(err)
3527
os.Exit(1)

relay/plugins/traffic/content-blocker-plugin/content-blocker-plugin.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,15 @@ func (plug contentBlockerPlugin) Name() string {
101101
return pluginName
102102
}
103103

104-
func (plug contentBlockerPlugin) HandleRequest(response http.ResponseWriter, request *http.Request, serviced bool) bool {
104+
func (plug contentBlockerPlugin) HandleRequest(
105+
response http.ResponseWriter,
106+
request *http.Request,
107+
info traffic.RequestInfo,
108+
) bool {
109+
if info.Serviced {
110+
return false
111+
}
112+
105113
if serviced := plug.blockHeaderContent(response, request); serviced {
106114
return true
107115
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package cookies_plugin
2+
3+
// The Cookies plugin provides the capability to allowlist cookies on incoming
4+
// requests. By default, all cookies are blocked. This is because in the context
5+
// of the relay, cookies are quite high-risk; it usually runs in a first-party
6+
// context, so the risk of receiving cookies that were intended for another
7+
// service is substantial.
8+
9+
import (
10+
"log"
11+
"net/http"
12+
"os"
13+
"strings"
14+
15+
"github.com/fullstorydev/relay-core/relay/commands"
16+
"github.com/fullstorydev/relay-core/relay/traffic"
17+
)
18+
19+
var (
20+
Factory cookiesPluginFactory
21+
logger = log.New(os.Stdout, "[traffic-cookies] ", 0)
22+
pluginName = "Cookies"
23+
)
24+
25+
type cookiesPluginFactory struct{}
26+
27+
func (f cookiesPluginFactory) Name() string {
28+
return pluginName
29+
}
30+
31+
func (f cookiesPluginFactory) New(env *commands.Environment) (traffic.Plugin, error) {
32+
plugin := &cookiesPlugin{
33+
allowlist: map[string]bool{},
34+
}
35+
36+
if cookiesVal, ok := env.LookupOptional("TRAFFIC_RELAY_COOKIES"); ok {
37+
for _, cookieName := range strings.Split(cookiesVal, " ") {
38+
logger.Printf(`Cookies plugin will allowlist cookie "%s"`, cookieName)
39+
plugin.allowlist[cookieName] = true
40+
}
41+
}
42+
43+
if len(plugin.allowlist) == 0 {
44+
return nil, nil
45+
}
46+
47+
return plugin, nil
48+
}
49+
50+
type cookiesPlugin struct {
51+
allowlist map[string]bool // The name of cookies that should be relayed.
52+
}
53+
54+
func (plug cookiesPlugin) Name() string {
55+
return pluginName
56+
}
57+
58+
func (plug cookiesPlugin) HandleRequest(
59+
response http.ResponseWriter,
60+
request *http.Request,
61+
info traffic.RequestInfo,
62+
) bool {
63+
if info.Serviced {
64+
return false
65+
}
66+
67+
// Restore the original Cookie header so that we can parse it using the
68+
// methods on http.Request.
69+
for _, headerValue := range info.OriginalCookieHeaders {
70+
request.Header.Add("Cookie", headerValue)
71+
}
72+
73+
// Parse the Cookie header and filter out cookies which aren't present in
74+
// the allowlist.
75+
var cookies []string
76+
for _, cookie := range request.Cookies() {
77+
if !plug.allowlist[cookie.Name] {
78+
continue
79+
}
80+
cookies = append(cookies, cookie.String())
81+
}
82+
83+
// Reserialize the Cookie header.
84+
request.Header.Set("Cookie", strings.Join(cookies, "; "))
85+
86+
return false
87+
}
88+
89+
/*
90+
Copyright 2022 FullStory, Inc.
91+
92+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
93+
and associated documentation files (the "Software"), to deal in the Software without restriction,
94+
including without limitation the rights to use, copy, modify, merge, publish, distribute,
95+
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
96+
furnished to do so, subject to the following conditions:
97+
98+
The above copyright notice and this permission notice shall be included in all copies or
99+
substantial portions of the Software.
100+
101+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
102+
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
103+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
104+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
105+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
106+
*/
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package cookies_plugin_test
2+
3+
import (
4+
"net/http"
5+
"reflect"
6+
"testing"
7+
8+
"github.com/fullstorydev/relay-core/catcher"
9+
"github.com/fullstorydev/relay-core/relay"
10+
"github.com/fullstorydev/relay-core/relay/plugins/traffic/cookies-plugin"
11+
"github.com/fullstorydev/relay-core/relay/test"
12+
"github.com/fullstorydev/relay-core/relay/traffic"
13+
)
14+
15+
func TestRelayedCookies(t *testing.T) {
16+
testCases := []struct {
17+
desc string
18+
env map[string]string
19+
originalCookieHeaders []string
20+
expectedCookieHeaders []string
21+
}{
22+
{
23+
desc: "No cookies are relayed by default",
24+
env: map[string]string{},
25+
originalCookieHeaders: []string{"SPECIAL_ID=298zf09hf012fh2; token=u32t4o3tb3gg43", "_gat=1"},
26+
expectedCookieHeaders: nil,
27+
},
28+
{
29+
desc: "Multiple Cookie headers are merged",
30+
env: map[string]string{
31+
"TRAFFIC_RELAY_COOKIES": "SPECIAL_ID token _gat",
32+
},
33+
originalCookieHeaders: []string{"SPECIAL_ID=298zf09hf012fh2; token=u32t4o3tb3gg43", "_gat=1"},
34+
expectedCookieHeaders: []string{"SPECIAL_ID=298zf09hf012fh2; token=u32t4o3tb3gg43; _gat=1"},
35+
},
36+
{
37+
desc: "Only allowlisted cookies are relayed",
38+
env: map[string]string{
39+
"TRAFFIC_RELAY_COOKIES": "SPECIAL_ID foo _gat",
40+
},
41+
originalCookieHeaders: []string{"SPECIAL_ID=298zf09hf012fh2; token=u32t4o3tb3gg43; foo=bar", "_gat=1; bar=foo"},
42+
expectedCookieHeaders: []string{"SPECIAL_ID=298zf09hf012fh2; foo=bar; _gat=1"},
43+
},
44+
{
45+
desc: "A Cookie header is dropped entirely when no cookies match",
46+
env: map[string]string{
47+
"TRAFFIC_RELAY_COOKIES": "bar",
48+
},
49+
originalCookieHeaders: []string{"SPECIAL_ID=298zf09hf012fh2; token=u32t4o3tb3gg43; foo=bar", "_gat=1; bar=foo"},
50+
expectedCookieHeaders: []string{"bar=foo"},
51+
},
52+
}
53+
54+
plugins := []traffic.PluginFactory{
55+
cookies_plugin.Factory,
56+
}
57+
58+
for _, testCase := range testCases {
59+
test.WithCatcherAndRelay(t, testCase.env, plugins, func(catcherService *catcher.Service, relayService *relay.Service) {
60+
request, err := http.NewRequest("GET", relayService.HttpUrl(), nil)
61+
if err != nil {
62+
t.Errorf("Test '%v': Error creating request: %v", testCase.desc, err)
63+
return
64+
}
65+
66+
for _, cookieHeaderValue := range testCase.originalCookieHeaders {
67+
request.Header.Add("Cookie", cookieHeaderValue)
68+
}
69+
70+
response, err := http.DefaultClient.Do(request)
71+
if err != nil {
72+
t.Errorf("Test '%v': Error GETing: %v", testCase.desc, err)
73+
return
74+
}
75+
defer response.Body.Close()
76+
77+
if response.StatusCode != 200 {
78+
t.Errorf("Test '%v': Expected 200 response: %v", testCase.desc, response)
79+
return
80+
}
81+
82+
lastRequest, err := catcherService.LastRequest()
83+
if err != nil {
84+
t.Errorf("Test '%v': Error reading last request from catcher: %v", testCase.desc, err)
85+
return
86+
}
87+
88+
actualCookieHeaders := lastRequest.Header["Cookie"]
89+
if !reflect.DeepEqual(testCase.expectedCookieHeaders, actualCookieHeaders) {
90+
t.Errorf(
91+
"Test '%v': Expected Cookie header values '%v' but got '%v'",
92+
testCase.desc,
93+
testCase.expectedCookieHeaders,
94+
actualCookieHeaders,
95+
)
96+
}
97+
})
98+
}
99+
}
100+
101+
/*
102+
Copyright 2022 FullStory, Inc.
103+
104+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
105+
and associated documentation files (the "Software"), to deal in the Software without restriction,
106+
including without limitation the rights to use, copy, modify, merge, publish, distribute,
107+
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
108+
furnished to do so, subject to the following conditions:
109+
110+
The above copyright notice and this permission notice shall be included in all copies or
111+
substantial portions of the Software.
112+
113+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
114+
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
115+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
116+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
117+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
118+
*/

0 commit comments

Comments
 (0)