forked from jc21/go-external-ip
-
Notifications
You must be signed in to change notification settings - Fork 0
/
consensus.go
162 lines (142 loc) · 4.53 KB
/
consensus.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
package externalip
import (
"log"
"net"
"sync"
"time"
)
// DefaultConsensusConfig returns the ConsensusConfig,
// with the default values:
// + Timeout: 30 seconds;
func DefaultConsensusConfig() *ConsensusConfig {
return &ConsensusConfig{
Timeout: time.Second * 30,
}
}
// DefaultConsensus returns a consensus filled
// with default and recommended HTTPSources.
// TLS-Protected providers get more power,
// compared to plain-text providers.
func DefaultConsensus(cfg *ConsensusConfig, logger *log.Logger) *Consensus {
consensus := NewConsensus(cfg, logger)
// TLS-protected providers
consensus.AddVoter(NewHTTPSource("https://icanhazip.com/"), 3)
consensus.AddVoter(NewHTTPSource("https://myexternalip.com/raw"), 3)
// Plain-text providers
consensus.AddVoter(NewHTTPSource("http://ifconfig.io/ip"), 1)
consensus.AddVoter(NewHTTPSource("http://checkip.amazonaws.com/"), 1)
consensus.AddVoter(NewHTTPSource("http://ident.me/"), 1)
consensus.AddVoter(NewHTTPSource("http://whatismyip.akamai.com/"), 1)
consensus.AddVoter(NewHTTPSource("http://tnx.nl/ip"), 1)
consensus.AddVoter(NewHTTPSource("http://myip.dnsomatic.com/"), 1)
consensus.AddVoter(NewHTTPSource("http://diagnostic.opendns.com/myip"), 1)
return consensus
}
// NewConsensus creates a new Consensus, with no sources.
// When the given cfg is <nil>, the `DefaultConsensusConfig` will be used.
func NewConsensus(cfg *ConsensusConfig, logger *log.Logger) *Consensus {
if cfg == nil {
cfg = DefaultConsensusConfig()
}
if logger == nil {
logger = NewLogger(nil)
}
return &Consensus{
timeout: cfg.Timeout,
logger: logger,
}
}
// ConsensusConfig is used to configure the Consensus, while creating it.
type ConsensusConfig struct {
Timeout time.Duration
}
// WithTimeout sets the voting timeout of this config,
// returning the config itself at the end, to allow for chaining
func (cfg *ConsensusConfig) WithTimeout(timeout time.Duration) *ConsensusConfig {
cfg.Timeout = timeout
return cfg
}
// Consensus the type at the center of this library,
// and is the main entry point for users.
// Its `ExternalIP` method allows you to ask for your ExternalIP,
// influenced by all its added voters.
type Consensus struct {
voters []voter
timeout time.Duration
logger *log.Logger
protocol uint
}
// AddVoter adds a voter to this consensus.
// The source cannot be <nil> and
// the weight has to be of a value of 1 or above.
func (c *Consensus) AddVoter(source Source, weight uint) error {
if source == nil {
c.logger.Println("[ERROR] could not add voter: no source given")
return ErrNoSource
}
if weight == 0 {
c.logger.Println("[ERROR] could not add voter: weight cannot be 0")
return ErrInsufficientWeight
}
c.voters = append(c.voters, voter{
source: source,
weight: weight,
})
return nil
}
// ExternalIP requests asynchronously the externalIP from all added voters,
// returning the IP which received the most votes.
// The returned IP will always be valid, in case the returned error is <nil>.
func (c *Consensus) ExternalIP() (net.IP, error) {
voteCollection := make(map[string]uint)
var vlock sync.Mutex
var wg sync.WaitGroup
// start all source Requests on a seperate goroutine
for _, v := range c.voters {
wg.Add(1)
go func(v voter) {
defer wg.Done()
ip, err := v.source.IP(c.timeout, c.logger, c.protocol)
if err == nil && ip != nil {
vlock.Lock()
defer vlock.Unlock()
voteCollection[ip.String()] += v.weight
}
}(v)
}
// wait for all votes to come in,
// or until their process times out
wg.Wait()
// if no votes were casted succesfully,
// return early with an error
if len(voteCollection) == 0 {
c.logger.Println("[ERROR] no votes were casted succesfully")
return nil, ErrNoIP
}
var max uint
var externalIP string
// find the IP which has received the most votes,
// influinced by the voter's weight.
vlock.Lock()
defer vlock.Unlock()
for ip, votes := range voteCollection {
if votes > max {
max, externalIP = votes, ip
}
}
// as the found IP was parsed previously,
// we know it cannot be nil and is valid
return net.ParseIP(externalIP), nil
}
// UseIPProtocol will set the IP Protocol to use for http requests
// to the sources. If zero, it will not discriminate. This is useful
// when you want to get the external IP in a specific protocol.
// Protocol only supports 0, 4 or 6.
func (c *Consensus) UseIPProtocol(protocol uint) error {
if protocol != 0 && protocol != 4 && protocol != 6 {
c.logger.Println("[ERROR] only ipv4 and ipv6 protocol is supported")
return ErrInvalidProtocol
}
c.protocol = protocol
return nil
}