Skip to content

Commit

Permalink
Add whitelists
Browse files Browse the repository at this point in the history
  • Loading branch information
lestrrat committed Sep 27, 2024
1 parent bf78d0a commit d00dac4
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 11 deletions.
14 changes: 6 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ func ExampleClient() {
json.NewEncoder(w).Encode(map[string]string{"hello": "world"})
}))

var options []httprc.NewClientOption
options := []httprc.NewClientOption{
httprc.WithWhitelist(httprc.NewInsecureWhitelist()),
}
// If you would like to handle errors from asynchronous workers, you can specify a error sink.
// This is disabled in this example because the trace logs are dynamic
// and thus would interfere with the runnable example test.
Expand All @@ -57,11 +59,7 @@ func ExampleClient() {
// dangling goroutines hanging around when you exit. For example, if you
// are running tests to check for goroutine leaks, you should call this
// function before the end of your test.
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = ctrl.Shutdown(ctx)
}()
defer ctrl.Shutdown(time.Second)

// Create a new resource that is synchronized every so often
r, err := httprc.NewResource[HelloWorld](srv.URL, httprc.JSONTransformer[HelloWorld]())
Expand All @@ -87,5 +85,5 @@ func ExampleClient() {
// world
}
```
source: [client_example_test.go](https://github.com/lestrrat-go/httprc/blob/v3-wip/client_example_test.go)
<!-- END INCLUDE -->
source: [client_example_test.go](https://github.com/lestrrat-go/httprc/blob/v3/client_example_test.go)
<!-- END INCLUDE -->
8 changes: 8 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Client struct {
running bool
errSink ErrorSink
traceSink TraceSink
wl Whitelist
}

const DefaultWorkers = 5
Expand All @@ -27,6 +28,9 @@ func NewClient(options ...NewClientOption) *Client {
var errSink ErrorSink = errsink.NewNop()
//nolint:stylecheck
var traceSink TraceSink = tracesink.NewNop()
//nolint:stylecheck

Check failure on line 31 in client.go

View workflow job for this annotation

GitHub Actions / lint

directive `//nolint:stylecheck` is unused for linter "stylecheck" (nolintlint)
var wl Whitelist = BlockAllWhitelist{}

numWorkers := DefaultWorkers
//nolint:forcetypeassert
for _, option := range options {
Expand All @@ -37,6 +41,8 @@ func NewClient(options ...NewClientOption) *Client {
errSink = option.Value().(ErrorSink)
case identTraceSink{}:
traceSink = option.Value().(TraceSink)
case identWhitelist{}:
wl = option.Value().(Whitelist)
}
}

Expand All @@ -47,6 +53,7 @@ func NewClient(options ...NewClientOption) *Client {
numWorkers: numWorkers,
errSink: errSink,
traceSink: traceSink,
wl: wl,
}
}

Expand Down Expand Up @@ -117,6 +124,7 @@ func (c *Client) Start(octx context.Context) (Controller, error) {
tickDuration: tickDuration,
check: time.NewTicker(tickDuration),
shutdown: make(chan struct{}),
wl: c.wl,
}
wg.Add(1)
go ctrl.loop(ctx, &wg)
Expand Down
4 changes: 3 additions & 1 deletion client_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ func ExampleClient() {
json.NewEncoder(w).Encode(map[string]string{"hello": "world"})
}))

var options []httprc.NewClientOption
options := []httprc.NewClientOption{
httprc.WithWhitelist(httprc.NewInsecureWhitelist()),
}
// If you would like to handle errors from asynchronous workers, you can specify a error sink.
// This is disabled in this example because the trace logs are dynamic
// and thus would interfere with the runnable example test.
Expand Down
6 changes: 6 additions & 0 deletions controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ type controller struct {
items []Resource
tickDuration time.Duration
shutdown chan struct{}

wl Whitelist
}

// Shutdown is a convenience function that calls ShutdownContext with a
Expand Down Expand Up @@ -74,6 +76,10 @@ type ctrlRequest struct {
// AddResource adds a new resource to the controller. If the resource already
// exists, it will return an error.
func (c *controller) AddResource(r Resource) error {
if !c.wl.IsAllowed(r.URL()) {
return fmt.Errorf(`httprc.Controller.AddResource: cannot add %q: %w`, r.URL(), errBlockedByWhitelist)
}

reply := make(chan error, 1)
c.incoming <- ctrlRequest{
op: addResource,
Expand Down
6 changes: 6 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,9 @@ var errRecoveredFromPanic = errors.New(`recovered from panic`)
func ErrRecoveredFromPanic() error {
return errRecoveredFromPanic
}

var errBlockedByWhitelist = errors.New(`blocked by whitelist`)

func ErrBlockedByWhitelist() error {
return errBlockedByWhitelist
}
10 changes: 8 additions & 2 deletions httprc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ func TestClient(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

cl := httprc.NewClient()
options := []httprc.NewClientOption{
httprc.WithWhitelist(httprc.NewInsecureWhitelist()),
}
cl := httprc.NewClient(options...)
ctrl, err := cl.Start(ctx)
require.NoError(t, err, `cl.Run should succeed`)
defer ctrl.Shutdown(time.Second)
Expand Down Expand Up @@ -130,7 +133,10 @@ func TestRefresh(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

cl := httprc.NewClient()
options := []httprc.NewClientOption{
httprc.WithWhitelist(httprc.NewInsecureWhitelist()),
}
cl := httprc.NewClient(options...)
ctrl, err := cl.Start(ctx)
require.NoError(t, err, `cl.Run should succeed`)
defer ctrl.Shutdown(time.Second)
Expand Down
8 changes: 8 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ func WithTraceSink(sink TraceSink) NewClientOption {
return newClientOption{option.New(identTraceSink{}, sink)}
}

type identWhitelist struct{}

// WithWhitelist specifies the whitelist to use for the client.
// If not specified, the client will use a BlockAllWhitelist.
func WithWhitelist(wl Whitelist) NewClientOption {
return newClientOption{option.New(identWhitelist{}, wl)}
}

type NewResourceOption interface {
option.Interface
newResourceOption()
Expand Down
109 changes: 109 additions & 0 deletions whitelist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package httprc

import (
"regexp"
"sync"
)

// Whitelist is an interface that allows you to determine if a given URL is allowed
// or not. Implementations of this interface can be used to restrict the URLs that
// the client can access.
//
// This exists because you might use this module to store resources provided by
// user of your application, in which case you cannot necessarily trust that the
// URLs are safe.
type Whitelist interface {
IsAllowed(string) bool
}

// WhitelistFunc is a function type that implements the Whitelist interface.
type WhitelistFunc func(string) bool

func (f WhitelistFunc) IsAllowed(u string) bool { return f(u) }

// BlockAllWhitelist is a Whitelist implementation that blocks all URLs. This is the
// default whitelist implementation.
type BlockAllWhitelist struct{}

// NewBlockAllWhitelist creates a new BlockAllWhitelist instance. It is safe to
// use the zero value of this type; this constructor is provided for consistency.
func NewBlockAllWhitelist() Whitelist { return BlockAllWhitelist{} }

func (BlockAllWhitelist) IsAllowed(_ string) bool { return false }

// InsecureWhitelist is a Whitelist implementation that allows all URLs. Be careful
// when using this in your production code: make sure you do not blindly register
// URLs from untrusted sources.
type InsecureWhitelist struct{}

// NewInsecureWhitelist creates a new InsecureWhitelist instance. It is safe to
// use the zero value of this type; this constructor is provided for consistency.
func NewInsecureWhitelist() Whitelist { return InsecureWhitelist{} }

func (InsecureWhitelist) IsAllowed(_ string) bool { return true }

// RegexpWhitelist is a jwk.Whitelist object comprised of a list of *regexp.Regexp
// objects. All entries in the list are tried until one matches. If none of the
// *regexp.Regexp objects match, then the URL is deemed unallowed.
type RegexpWhitelist struct {
mu sync.RWMutex
patterns []*regexp.Regexp
}

// NewRegexpWhitelist creates a new RegexpWhitelist instance. It is safe to use the
// zero value of this type; this constructor is provided for consistency.
func NewRegexpWhitelist() *RegexpWhitelist {
return &RegexpWhitelist{}
}

// Add adds a new regular expression to the list of expressions to match against.
func (w *RegexpWhitelist) Add(pat *regexp.Regexp) *RegexpWhitelist {
w.mu.Lock()
defer w.mu.Unlock()
w.patterns = append(w.patterns, pat)
return w
}

// IsAllowed returns true if any of the patterns in the whitelist
// returns true.
func (w *RegexpWhitelist) IsAllowed(u string) bool {
w.mu.RLock()
patterns := w.patterns
w.mu.RUnlock()
for _, pat := range patterns {
if pat.MatchString(u) {
return true
}
}
return false
}

// MapWhitelist is a jwk.Whitelist object comprised of a map of strings.
// If the URL exists in the map, then the URL is allowed to be fetched.
type MapWhitelist interface {
Whitelist
Add(string) MapWhitelist
}

type mapWhitelist struct {
mu sync.RWMutex
store map[string]struct{}
}

func NewMapWhitelist() MapWhitelist {
return &mapWhitelist{store: make(map[string]struct{})}
}

func (w *mapWhitelist) Add(pat string) MapWhitelist {
w.mu.Lock()
defer w.mu.Unlock()
w.store[pat] = struct{}{}
return w
}

func (w *mapWhitelist) IsAllowed(u string) bool {
w.mu.RLock()
_, b := w.store[u]
w.mu.RUnlock()
return b
}

0 comments on commit d00dac4

Please sign in to comment.