Skip to content

Commit 48d9120

Browse files
author
Ryan P
authored
Add HTTP port rules to HTTPFilter (#185) (#210)
1 parent 7579e08 commit 48d9120

File tree

4 files changed

+157
-0
lines changed

4 files changed

+157
-0
lines changed

rcap/http_rulefilter.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,36 @@ package rcap
33
import (
44
"net"
55
"net/http"
6+
"net/url"
7+
"strconv"
68
"strings"
79

810
"github.com/pkg/errors"
11+
12+
"github.com/suborbital/reactr/util"
913
)
1014

1115
var (
1216
ErrHttpDisallowed = errors.New("requests to insecure HTTP endpoints is disallowed")
1317
ErrIPsDisallowed = errors.New("requests to IP addresses are disallowed")
1418
ErrPrivateDisallowed = errors.New("requests to private IP address ranges are disallowed")
1519
ErrDomainDisallowed = errors.New("requests to this domain are disallowed")
20+
ErrPortDisallowed = errors.New("requests to this port are disallowed")
1621
)
1722

1823
// HTTPRules is a set of rules that governs use of the HTTP capability
1924
type HTTPRules struct {
2025
AllowedDomains []string `json:"allowedDomains" yaml:"allowedDomains"`
2126
BlockedDomains []string `json:"blockedDomains" yaml:"blockedDomains"`
27+
AllowedPorts []int `json:"allowedPorts" yaml:"allowedPorts"`
28+
BlockedPorts []int `json:"blockedPorts" yaml:"blockedPorts"`
2229
AllowIPs bool `json:"allowIPs" yaml:"allowIPs"`
2330
AllowPrivate bool `json:"allowPrivate" yaml:"allowPrivate"`
2431
AllowHTTP bool `json:"allowHTTP" yaml:"allowHTTP"`
2532
}
2633

34+
var standardPorts = []int{80, 443}
35+
2736
// requestIsAllowed returns a non-nil error if the provided request is not allowed to proceed
2837
func (h HTTPRules) requestIsAllowed(req *http.Request) error {
2938
// Hostname removes port numbers as well as IPv6 [ and ]
@@ -35,6 +44,11 @@ func (h HTTPRules) requestIsAllowed(req *http.Request) error {
3544
}
3645
}
3746

47+
// Evaluate port access rules
48+
if err := h.portAllowed(req.URL); err != nil {
49+
return err
50+
}
51+
3852
// determine if the passed-in host is an IP address
3953
isRawIP := net.ParseIP(req.URL.Hostname()) != nil
4054
if !h.AllowIPs && isRawIP {
@@ -90,6 +104,44 @@ func (h HTTPRules) requestIsAllowed(req *http.Request) error {
90104
return nil
91105
}
92106

107+
// portAllowed evaluates port allowance rules
108+
func (h HTTPRules) portAllowed(url *url.URL) error {
109+
// Backward Compatibility:
110+
// Allow all ports if no allow/block list has been configured
111+
if len(h.AllowedPorts)+len(h.BlockedPorts) == 0 {
112+
return nil
113+
}
114+
115+
port, err := readPort(url)
116+
if err != nil {
117+
return ErrPortDisallowed
118+
}
119+
120+
if util.ContainsInt(port, h.BlockedPorts) {
121+
return ErrPortDisallowed
122+
}
123+
124+
for _, p := range append(standardPorts, h.AllowedPorts...) {
125+
if p == port {
126+
return nil
127+
}
128+
}
129+
130+
return ErrPortDisallowed
131+
}
132+
133+
// readPort returns normalized URL port
134+
func readPort(url *url.URL) (int, error) {
135+
if url.Port() == "" {
136+
if url.Scheme == "https" {
137+
return 443, nil
138+
}
139+
return 80, nil
140+
}
141+
142+
return strconv.Atoi(url.Port())
143+
}
144+
93145
// returns nil if the host does not resolve to an IP in a private range
94146
// returns ErrPrivateDisallowed if it does
95147
func resolvesToPrivate(host string) error {

rcap/http_rulefilter_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,89 @@ func TestBlockedDomains(t *testing.T) {
187187
})
188188
}
189189

190+
func TestAllowedPorts(t *testing.T) {
191+
rules := defaultHTTPRules()
192+
rules.AllowedPorts = []int{8080}
193+
194+
t.Run("standard http port allowed", func(t *testing.T) {
195+
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
196+
197+
if err := rules.requestIsAllowed(req); err != nil {
198+
t.Error("error occurred, should not have:", err)
199+
}
200+
})
201+
202+
t.Run("standard https port allowed", func(t *testing.T) {
203+
req, _ := http.NewRequest(http.MethodGet, "https://example.com", nil)
204+
205+
if err := rules.requestIsAllowed(req); err != nil {
206+
t.Error("error occurred, should not have:", err)
207+
}
208+
})
209+
210+
t.Run("port 8080 allowed", func(t *testing.T) {
211+
req, _ := http.NewRequest(http.MethodGet, "http://example.com:8080", nil)
212+
213+
if err := rules.requestIsAllowed(req); err != nil {
214+
t.Error("error occurred, should not have:", err)
215+
}
216+
})
217+
218+
t.Run("port 8088 disallowed", func(t *testing.T) {
219+
req, _ := http.NewRequest(http.MethodGet, "http://example.com:8088", nil)
220+
221+
if err := rules.requestIsAllowed(req); err == nil {
222+
t.Error("error did not occur, should have")
223+
}
224+
})
225+
}
226+
227+
func TestBlockedPorts(t *testing.T) {
228+
rules := defaultHTTPRules()
229+
rules.AllowedPorts = []int{8081, 8082}
230+
rules.BlockedPorts = []int{80, 443, 8080, 8081}
231+
232+
t.Run("standard HTTP port disallowed", func(t *testing.T) {
233+
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
234+
235+
if err := rules.requestIsAllowed(req); err == nil {
236+
t.Error("error did not occur, should have")
237+
}
238+
})
239+
240+
t.Run("standard HTTPS port disallowed", func(t *testing.T) {
241+
req, _ := http.NewRequest(http.MethodGet, "https://example.com", nil)
242+
243+
if err := rules.requestIsAllowed(req); err == nil {
244+
t.Error("error did not occur, should have")
245+
}
246+
})
247+
248+
t.Run("port 8080 disallowed", func(t *testing.T) {
249+
req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil)
250+
251+
if err := rules.requestIsAllowed(req); err == nil {
252+
t.Error("error did not occur, should have")
253+
}
254+
})
255+
256+
t.Run("blocked list takes precedence over allow list", func(t *testing.T) {
257+
req, _ := http.NewRequest(http.MethodGet, "http://example.com:8081", nil)
258+
259+
if err := rules.requestIsAllowed(req); err == nil {
260+
t.Error("error did not occur, should have")
261+
}
262+
})
263+
264+
t.Run("port 8082 allowed", func(t *testing.T) {
265+
req, _ := http.NewRequest(http.MethodGet, "http://example.com:8082", nil)
266+
267+
if err := rules.requestIsAllowed(req); err != nil {
268+
t.Error("error occurred, should not have:", err)
269+
}
270+
})
271+
}
272+
190273
func TestBlockedWithCNAME(t *testing.T) {
191274
rules := defaultHTTPRules()
192275
rules.BlockedDomains = []string{"hosting.gitbook.io"}

util/util.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,13 @@ func GenerateResultID() string {
2020

2121
return id
2222
}
23+
24+
// ContainsInt returns true if value present in int slice
25+
func ContainsInt(value int, values []int) bool {
26+
for _, p := range values {
27+
if p == value {
28+
return true
29+
}
30+
}
31+
return false
32+
}

util/util_test.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,15 @@ func TestGenerateResultID(t *testing.T) {
1111
t.Errorf("id has length %d, expected 24", len(id))
1212
}
1313
}
14+
15+
func TestContainsInt(t *testing.T) {
16+
container := []int{1, 2, 3, 4}
17+
18+
if !ContainsInt(3, container) {
19+
t.Errorf("expected value not found in container")
20+
}
21+
22+
if ContainsInt(5, container) {
23+
t.Errorf("should not have found value in container")
24+
}
25+
}

0 commit comments

Comments
 (0)