1// Copyright (C) 2019 Storj Labs, Inc.
2// See LICENSE for copying information.
3
4package trust
5
6import (
7	"net"
8	"strconv"
9	"strings"
10
11	"github.com/zeebo/errs"
12
13	"storj.io/common/storj"
14)
15
16var (
17	// ErrExclusion is an error class for exclusion related errors.
18	ErrExclusion = errs.Class("exclusion")
19)
20
21// NewExcluder takes a configuration string and returns an excluding Rule.
22// Accepted forms are 1) a Satellite ID followed by '@', 2) a hostname or IP
23// address, 3) a full Satellite URL.
24func NewExcluder(config string) (Rule, error) {
25	url, err := parseExcluderConfig(config)
26	if err != nil {
27		return nil, err
28	}
29
30	switch {
31	case url.Host == "":
32		return NewIDExcluder(url.ID), nil
33	case url.ID.IsZero():
34		return NewHostExcluder(url.Host), nil
35	default:
36		return NewURLExcluder(url), nil
37	}
38}
39
40// URLExcluder excludes matching URLs.
41type URLExcluder struct {
42	url SatelliteURL
43}
44
45// NewURLExcluder returns a new URLExcluder.
46func NewURLExcluder(url SatelliteURL) *URLExcluder {
47	url.Host = normalizeHost(url.Host)
48	return &URLExcluder{
49		url: url,
50	}
51}
52
53// IsTrusted returns true if the given Satellite is trusted and false otherwise.
54func (excluder *URLExcluder) IsTrusted(url SatelliteURL) bool {
55	url.Host = normalizeHost(url.Host)
56	return excluder.url != url
57}
58
59// String returns a string representation of the excluder.
60func (excluder *URLExcluder) String() string {
61	return excluder.url.String()
62}
63
64// IDExcluder excludes URLs matching a given URL.
65type IDExcluder struct {
66	id storj.NodeID
67}
68
69// NewIDExcluder returns a new IDExcluder.
70func NewIDExcluder(id storj.NodeID) *IDExcluder {
71	return &IDExcluder{
72		id: id,
73	}
74}
75
76// IsTrusted returns true if the given Satellite is trusted and false otherwise.
77func (excluder *IDExcluder) IsTrusted(url SatelliteURL) bool {
78	return excluder.id != url.ID
79}
80
81// String returns a string representation of the excluder.
82func (excluder *IDExcluder) String() string {
83	return excluder.id.String() + "@"
84}
85
86// HostExcluder excludes URLs that match a given host. If the host is a domain
87// name then URLs in a subdomain of that domain are excluded as well.
88type HostExcluder struct {
89	host   string
90	suffix string
91}
92
93// NewHostExcluder returns a new HostExcluder.
94func NewHostExcluder(host string) *HostExcluder {
95	host = normalizeHost(host)
96
97	// If it appears to be a domain name (i.e. has a dot) then configure the
98	// suffix as well
99	var suffix string
100	if strings.ContainsRune(host, '.') {
101		suffix = "." + host
102	}
103	return &HostExcluder{
104		host:   host,
105		suffix: suffix,
106	}
107}
108
109// IsTrusted returns true if the given Satellite is trusted and false otherwise.
110func (excluder *HostExcluder) IsTrusted(url SatelliteURL) bool {
111	host := normalizeHost(url.Host)
112	if excluder.host == host {
113		return false
114	}
115
116	if excluder.suffix != "" && strings.HasSuffix(host, excluder.suffix) {
117		return false
118	}
119	return true
120}
121
122// String returns a string representation of the excluder.
123func (excluder *HostExcluder) String() string {
124	return excluder.host
125}
126
127// parseExcluderConfig parses a excluder configuration. The following forms are accepted:
128// - Satellite ID followed by @
129// - Satellite host
130// - Full Satellite URL (i.e. id@host:port).
131func parseExcluderConfig(s string) (SatelliteURL, error) {
132	url, err := storj.ParseNodeURL(s)
133	if err != nil {
134		return SatelliteURL{}, ErrExclusion.Wrap(err)
135	}
136
137	switch {
138	case url.ID.IsZero() && url.Address != "":
139		// Just the address was specified. Ensure it does not have a port.
140		_, _, err := net.SplitHostPort(url.Address)
141		if err == nil {
142			return SatelliteURL{}, ErrExclusion.New("host exclusion must not include a port")
143		}
144		return SatelliteURL{
145			Host: url.Address,
146		}, nil
147	case !url.ID.IsZero() && url.Address == "":
148		// Just the ID was specified.
149		return SatelliteURL{
150			ID: url.ID,
151		}, nil
152	}
153
154	// storj.ParseNodeURL will have already verified that the address is
155	// well-formed, so if SplitHostPort fails it should be due to the address
156	// not having a port.
157	host, portStr, err := net.SplitHostPort(url.Address)
158	if err != nil {
159		return SatelliteURL{}, ErrExclusion.New("satellite URL exclusion must specify a port")
160	}
161
162	// Port should already be numeric so this shouldn't fail, but just in case.
163	port, err := strconv.Atoi(portStr)
164	if err != nil {
165		return SatelliteURL{}, ErrExclusion.New("satellite URL exclusion port is not numeric")
166	}
167
168	return SatelliteURL{
169		ID:   url.ID,
170		Host: host,
171		Port: port,
172	}, nil
173}
174