storagenode/trust: rule and excluders
Change-Id: I84ed542e1ef3cfaa5cc3d3f631cdc295393bf978
This commit is contained in:
parent
5d8d9cd89f
commit
715d97e3d8
173
storagenode/trust/excluder.go
Normal file
173
storagenode/trust/excluder.go
Normal file
@ -0,0 +1,173 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package trust
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/zeebo/errs"
|
||||
|
||||
"storj.io/storj/pkg/storj"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrExclusion is an error class for exclusion related errors
|
||||
ErrExclusion = errs.Class("exclusion")
|
||||
)
|
||||
|
||||
// NewExcluder takes a configuration string and returns an excluding Rule.
|
||||
// Accepted forms are 1) a Satellite ID followed by '@', 2) a hostname or IP
|
||||
// address, 3) a full Satellite URL.
|
||||
func NewExcluder(config string) (Rule, error) {
|
||||
url, err := parseExcluderConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case url.Host == "":
|
||||
return NewIDExcluder(url.ID), nil
|
||||
case url.ID.IsZero():
|
||||
return NewHostExcluder(url.Host), nil
|
||||
default:
|
||||
return NewURLExcluder(url), nil
|
||||
}
|
||||
}
|
||||
|
||||
// URLExcluder excludes matching URLs
|
||||
type URLExcluder struct {
|
||||
url SatelliteURL
|
||||
}
|
||||
|
||||
// NewURLExcluder returns a new URLExcluder
|
||||
func NewURLExcluder(url SatelliteURL) *URLExcluder {
|
||||
url.Host = normalizeHost(url.Host)
|
||||
return &URLExcluder{
|
||||
url: url,
|
||||
}
|
||||
}
|
||||
|
||||
// IsTrusted returns true if the given Satellite is trusted and false otherwise
|
||||
func (excluder *URLExcluder) IsTrusted(url SatelliteURL) bool {
|
||||
url.Host = normalizeHost(url.Host)
|
||||
return excluder.url != url
|
||||
}
|
||||
|
||||
// String returns a string representation of the excluder
|
||||
func (excluder *URLExcluder) String() string {
|
||||
return excluder.url.String()
|
||||
}
|
||||
|
||||
// IDExcluder excludes URLs matching a given URL
|
||||
type IDExcluder struct {
|
||||
id storj.NodeID
|
||||
}
|
||||
|
||||
// NewIDExcluder returns a new IDExcluder
|
||||
func NewIDExcluder(id storj.NodeID) *IDExcluder {
|
||||
return &IDExcluder{
|
||||
id: id,
|
||||
}
|
||||
}
|
||||
|
||||
// IsTrusted returns true if the given Satellite is trusted and false otherwise
|
||||
func (excluder *IDExcluder) IsTrusted(url SatelliteURL) bool {
|
||||
return excluder.id != url.ID
|
||||
}
|
||||
|
||||
// String returns a string representation of the excluder
|
||||
func (excluder *IDExcluder) String() string {
|
||||
return excluder.id.String() + "@"
|
||||
}
|
||||
|
||||
// HostExcluder excludes URLs that match a given host. If the host is a domain
|
||||
// name then URLs in a subdomain of that domain are excluded as well.
|
||||
type HostExcluder struct {
|
||||
host string
|
||||
suffix string
|
||||
}
|
||||
|
||||
// NewHostExcluder returns a new HostExcluder
|
||||
func NewHostExcluder(host string) *HostExcluder {
|
||||
host = normalizeHost(host)
|
||||
|
||||
// If it appears to be a domain name (i.e. has a dot) then configure the
|
||||
// suffix as well
|
||||
var suffix string
|
||||
if strings.ContainsRune(host, '.') {
|
||||
suffix = "." + host
|
||||
}
|
||||
return &HostExcluder{
|
||||
host: host,
|
||||
suffix: suffix,
|
||||
}
|
||||
}
|
||||
|
||||
// IsTrusted returns true if the given Satellite is trusted and false otherwise
|
||||
func (excluder *HostExcluder) IsTrusted(url SatelliteURL) bool {
|
||||
host := normalizeHost(url.Host)
|
||||
if excluder.host == host {
|
||||
return false
|
||||
}
|
||||
|
||||
if excluder.suffix != "" && strings.HasSuffix(host, excluder.suffix) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// String returns a string representation of the excluder
|
||||
func (excluder *HostExcluder) String() string {
|
||||
return excluder.host
|
||||
}
|
||||
|
||||
// parseExcluderConfig parses a excluder configuration. The following forms are accepted:
|
||||
// - Satellite ID followed by @
|
||||
// - Satellite host
|
||||
// - Full Satellite URL (i.e. id@host:port)
|
||||
func parseExcluderConfig(s string) (SatelliteURL, error) {
|
||||
url, err := storj.ParseNodeURL(s)
|
||||
if err != nil {
|
||||
return SatelliteURL{}, ErrExclusion.Wrap(err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case url.ID.IsZero() && url.Address != "":
|
||||
// Just the address was specified. Ensure it does not have a port.
|
||||
_, _, err := net.SplitHostPort(url.Address)
|
||||
if err == nil {
|
||||
return SatelliteURL{}, ErrExclusion.New("host exclusion must not include a port")
|
||||
}
|
||||
return SatelliteURL{
|
||||
Host: url.Address,
|
||||
}, nil
|
||||
case !url.ID.IsZero() && url.Address == "":
|
||||
// Just the ID was specified.
|
||||
return SatelliteURL{
|
||||
ID: url.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// storj.ParseNodeURL will have already verified that the address is
|
||||
// well-formed, so if SplitHostPort fails it should be due to the address
|
||||
// not having a port.
|
||||
host, portStr, err := net.SplitHostPort(url.Address)
|
||||
if err != nil {
|
||||
return SatelliteURL{}, ErrExclusion.New("satellite URL exclusion must specify a port")
|
||||
}
|
||||
|
||||
// Port should already be numeric so this shouldn't fail, but just in case.
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return SatelliteURL{}, ErrExclusion.New("satellite URL exclusion port is not numeric")
|
||||
}
|
||||
|
||||
return SatelliteURL{
|
||||
ID: url.ID,
|
||||
Host: host,
|
||||
Port: port,
|
||||
}, nil
|
||||
}
|
153
storagenode/trust/excluder_test.go
Normal file
153
storagenode/trust/excluder_test.go
Normal file
@ -0,0 +1,153 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package trust_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"storj.io/storj/storagenode/trust"
|
||||
)
|
||||
|
||||
func TestNewExcluderFailure(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
config string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "not a valid URL",
|
||||
config: "://",
|
||||
err: "exclusion: node URL error: parse ://: missing protocol scheme",
|
||||
},
|
||||
{
|
||||
name: "host exclusion must not include a port",
|
||||
config: "bar.test:7777",
|
||||
err: "exclusion: host exclusion must not include a port",
|
||||
},
|
||||
{
|
||||
name: "satellite URL exclusion must specify a port",
|
||||
config: "121RTSDpyNZVcEU84Ticf2L1ntiuUimbWgfATz21tuvgk3vzoA6@bar.test",
|
||||
err: "exclusion: satellite URL exclusion must specify a port",
|
||||
},
|
||||
} {
|
||||
tt := tt // quiet linting
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := trust.NewExcluder(tt.config)
|
||||
require.EqualError(t, err, tt.err)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNewExcluder(t *testing.T) {
|
||||
goodURL := makeSatelliteURL("foo.test")
|
||||
badURL := makeSatelliteURL("b.bar.test")
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
config string
|
||||
}{
|
||||
{
|
||||
name: "filtered by id",
|
||||
config: badURL.ID.String() + "@",
|
||||
},
|
||||
{
|
||||
name: "filtered by root domain",
|
||||
config: "bar.test",
|
||||
},
|
||||
{
|
||||
name: "filtered by exact domain",
|
||||
config: "b.bar.test",
|
||||
},
|
||||
{
|
||||
name: "filtered by full url",
|
||||
config: badURL.String(),
|
||||
},
|
||||
} {
|
||||
tt := tt // quiet linting
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
excluder, err := trust.NewExcluder(tt.config)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, excluder.IsTrusted(goodURL), "good URL should not be excluded")
|
||||
assert.False(t, excluder.IsTrusted(badURL), "bad URL should be excluded")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostExcluder(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
exclusion string
|
||||
host string
|
||||
isTrusted bool
|
||||
}{
|
||||
{
|
||||
exclusion: "foo.test",
|
||||
host: "foo.test",
|
||||
isTrusted: false,
|
||||
},
|
||||
{
|
||||
exclusion: "foo.test",
|
||||
host: "x.foo.test",
|
||||
isTrusted: false,
|
||||
},
|
||||
{
|
||||
exclusion: "foo.test",
|
||||
host: ".foo.test",
|
||||
isTrusted: false,
|
||||
},
|
||||
{
|
||||
exclusion: "foo.test",
|
||||
host: "foo.test.",
|
||||
isTrusted: false,
|
||||
},
|
||||
{
|
||||
exclusion: "x.bar.test",
|
||||
host: "bar.test",
|
||||
isTrusted: true,
|
||||
},
|
||||
{
|
||||
exclusion: "x.bar.test",
|
||||
host: "x.bar.test",
|
||||
isTrusted: false,
|
||||
},
|
||||
{
|
||||
exclusion: "x.bar.test",
|
||||
host: "y.x.bar.test",
|
||||
isTrusted: false,
|
||||
},
|
||||
{
|
||||
exclusion: ".baz.test",
|
||||
host: "baz.test",
|
||||
isTrusted: false,
|
||||
},
|
||||
{
|
||||
exclusion: "baz.test.",
|
||||
host: "baz.test",
|
||||
isTrusted: false,
|
||||
},
|
||||
{
|
||||
exclusion: "satellite",
|
||||
host: "satellite",
|
||||
isTrusted: false,
|
||||
},
|
||||
{
|
||||
exclusion: "satellite",
|
||||
host: "x.satellite",
|
||||
isTrusted: true,
|
||||
},
|
||||
} {
|
||||
tt := tt // quiet linting
|
||||
name := fmt.Sprintf("%s-%s-%t", tt.exclusion, tt.host, tt.isTrusted)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
excluder := trust.NewHostExcluder(tt.exclusion)
|
||||
isTrusted := excluder.IsTrusted(trust.SatelliteURL{Host: tt.host})
|
||||
assert.Equal(t, tt.isTrusted, isTrusted)
|
||||
})
|
||||
}
|
||||
}
|
26
storagenode/trust/rule.go
Normal file
26
storagenode/trust/rule.go
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package trust
|
||||
|
||||
// Rule indicates whether or not a Satellite URL is trusted
|
||||
type Rule interface {
|
||||
// IsTrusted returns true if the given Satellite is trusted and false otherwise
|
||||
IsTrusted(url SatelliteURL) bool
|
||||
|
||||
// String returns a string representation of the rule
|
||||
String() string
|
||||
}
|
||||
|
||||
// Rules is a collection of rules
|
||||
type Rules []Rule
|
||||
|
||||
// IsTrusted returns true if the given Satellite is trusted and false otherwise
|
||||
func (rules Rules) IsTrusted(url SatelliteURL) bool {
|
||||
for _, rule := range rules {
|
||||
if !rule.IsTrusted(url) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
40
storagenode/trust/rule_test.go
Normal file
40
storagenode/trust/rule_test.go
Normal file
@ -0,0 +1,40 @@
|
||||
// Copyright (C) 2019 Storj Labs, Inc.
|
||||
// See LICENSE for copying information.
|
||||
|
||||
package trust_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"storj.io/storj/storagenode/trust"
|
||||
)
|
||||
|
||||
func TestRulesIsTrusted(t *testing.T) {
|
||||
|
||||
url := makeSatelliteURL("domain.test")
|
||||
|
||||
// default is trusted when there are no rules
|
||||
var rules trust.Rules
|
||||
assert.True(t, rules.IsTrusted(url))
|
||||
|
||||
rules = trust.Rules{fakeRule(true)}
|
||||
assert.True(t, rules.IsTrusted(url))
|
||||
|
||||
rules = trust.Rules{fakeRule(false)}
|
||||
assert.False(t, rules.IsTrusted(url))
|
||||
|
||||
rules = trust.Rules{fakeRule(true), fakeRule(false), fakeRule(true)}
|
||||
assert.False(t, rules.IsTrusted(url))
|
||||
}
|
||||
|
||||
type fakeRule bool
|
||||
|
||||
func (rule fakeRule) IsTrusted(url trust.SatelliteURL) bool {
|
||||
return bool(rule)
|
||||
}
|
||||
|
||||
func (rule fakeRule) String() string {
|
||||
return "fake"
|
||||
}
|
Loading…
Reference in New Issue
Block a user