simplify and improve the design

This commit is contained in:
decauwsemaecker.glen@gmail.com 2017-04-06 11:52:19 -05:00
parent 12729cd206
commit 71ad8c93d9
3 changed files with 60 additions and 75 deletions

View File

@ -1,17 +1,18 @@
package externalip package externalip
import ( import (
"fmt"
"net" "net"
"net/http" "sync"
"time" "time"
) )
// DefaultConsensusConfig returns the ConsensusConfig, // DefaultConsensusConfig returns the ConsensusConfig,
// with the default values: // with the default values:
// + Timeout: 5 seconds; // + Timeout: 30 seconds;
func DefaultConsensusConfig() *ConsensusConfig { func DefaultConsensusConfig() *ConsensusConfig {
return &ConsensusConfig{ return &ConsensusConfig{
Timeout: time.Second * 5, Timeout: time.Second * 30,
} }
} }
@ -23,18 +24,18 @@ func DefaultConsensus(cfg *ConsensusConfig) *Consensus {
consensus := NewConsensus(cfg) consensus := NewConsensus(cfg)
// TLS-protected providers // TLS-protected providers
consensus.AddHTTPVoter("https://icanhazip.com/", 3) consensus.AddVoter(NewHTTPSource("https://icanhazip.com/"), 3)
consensus.AddHTTPVoter("https://myexternalip.com/raw", 3) consensus.AddVoter(NewHTTPSource("https://myexternalip.com/raw"), 3)
// Plain-text providers // Plain-text providers
consensus.AddHTTPVoter("http://ifconfig.io/ip", 1) consensus.AddVoter(NewHTTPSource("http://ifconfig.io/ip"), 1)
consensus.AddHTTPVoter("http://checkip.amazonaws.com/", 1) consensus.AddVoter(NewHTTPSource("http://checkip.amazonaws.com/"), 1)
consensus.AddHTTPVoter("http://ident.me/", 1) consensus.AddVoter(NewHTTPSource("http://ident.me/"), 1)
consensus.AddHTTPVoter("http://whatismyip.akamai.com/", 1) consensus.AddVoter(NewHTTPSource("http://whatismyip.akamai.com/"), 1)
consensus.AddHTTPVoter("http://tnx.nl/ip", 1) consensus.AddVoter(NewHTTPSource("http://tnx.nl/ip"), 1)
consensus.AddHTTPVoter("http://myip.dnsomatic.com/", 1) consensus.AddVoter(NewHTTPSource("http://myip.dnsomatic.com/"), 1)
consensus.AddHTTPVoter("http://ipecho.net/plain", 1) consensus.AddVoter(NewHTTPSource("http://ipecho.net/plain"), 1)
consensus.AddHTTPVoter("http://diagnostic.opendns.com/myip", 1) consensus.AddVoter(NewHTTPSource("http://diagnostic.opendns.com/myip"), 1)
return consensus return consensus
} }
@ -46,7 +47,7 @@ func NewConsensus(cfg *ConsensusConfig) *Consensus {
cfg = DefaultConsensusConfig() cfg = DefaultConsensusConfig()
} }
return &Consensus{ return &Consensus{
client: &http.Client{Timeout: cfg.Timeout}, timeout: cfg.Timeout,
} }
} }
@ -55,7 +56,7 @@ type ConsensusConfig struct {
Timeout time.Duration Timeout time.Duration
} }
// WithTimeout sets the timeout of this config, // WithTimeout sets the voting timeout of this config,
// returning the config itself at the end, to allow for chaining // returning the config itself at the end, to allow for chaining
func (cfg *ConsensusConfig) WithTimeout(timeout time.Duration) *ConsensusConfig { func (cfg *ConsensusConfig) WithTimeout(timeout time.Duration) *ConsensusConfig {
cfg.Timeout = timeout cfg.Timeout = timeout
@ -67,8 +68,8 @@ func (cfg *ConsensusConfig) WithTimeout(timeout time.Duration) *ConsensusConfig
// Its `ExternalIP` method allows you to ask for your ExternalIP, // Its `ExternalIP` method allows you to ask for your ExternalIP,
// influenced by all its added voters. // influenced by all its added voters.
type Consensus struct { type Consensus struct {
voters []voter voters []voter
client *http.Client timeout time.Duration
} }
// AddVoter adds a voter to this consensus. // AddVoter adds a voter to this consensus.
@ -89,52 +90,35 @@ func (c *Consensus) AddVoter(source Source, weight uint) error {
return nil return nil
} }
// AddHTTPVoter creates and adds an HTTP Voter to this consensus,
// using the HTTP Client of this Consensus, configured by the ConsensusConfig.
func (c *Consensus) AddHTTPVoter(url string, weight uint) error {
return c.AddVoter(NewHTTPSource(c.client, url), weight)
}
// AddComplexHTTPVoter creates an adds an HTTP Voter to this consensus,
// using a given parser, and the HTTP Client of this Consensus,
// configured by the ConsensusConfig
func (c *Consensus) AddComplexHTTPVoter(url string, parser ContentParser, weight uint) error {
return c.AddVoter(
NewHTTPSource(c.client, url).WithParser(parser),
weight,
)
}
// ExternalIP requests asynchronously the externalIP from all added voters, // ExternalIP requests asynchronously the externalIP from all added voters,
// returning the IP which received the most votes. // returning the IP which received the most votes.
// The returned IP will always be valid, in case the returned error is <nil>. // The returned IP will always be valid, in case the returned error is <nil>.
func (c *Consensus) ExternalIP() (net.IP, error) { func (c *Consensus) ExternalIP() (net.IP, error) {
voteCollection := make(map[string]uint) voteCollection := make(map[string]uint)
ch := make(chan vote, len(c.voters)) var vlock sync.Mutex
var wg sync.WaitGroup
// start all source Requests on a seperate goroutine // start all source Requests on a seperate goroutine
for _, v := range c.voters { for _, v := range c.voters {
wg.Add(1)
go func(v voter) { go func(v voter) {
vote := vote{ defer wg.Done()
Count: v.weight, ip, err := v.source.IP(c.timeout)
Error: InvalidIPError(""), if err == nil && ip != nil {
} vlock.Lock()
defer func() { defer vlock.Unlock()
ch <- vote voteCollection[ip.String()] += v.weight
}()
vote.IP, vote.Error = v.source.IP()
if vote.Error == nil && vote.IP == nil {
vote.Error = InvalidIPError("")
} }
}(v) }(v)
} }
// Wait for all votes to come in // wait for all votes to come in,
for range c.voters { // or until the voting process times out
vote := <-ch select {
if vote.Error == nil { case <-waitWG(&wg):
voteCollection[vote.IP.String()] += vote.Count fmt.Println("done!") // TODO: Log instead
} case <-time.After(c.timeout):
fmt.Println("timeout!") // TODO: Log instead
} }
// if no votes were casted succesfully, // if no votes were casted succesfully,
@ -148,6 +132,8 @@ func (c *Consensus) ExternalIP() (net.IP, error) {
// find the IP which has received the most votes, // find the IP which has received the most votes,
// influinced by the voter's weight. // influinced by the voter's weight.
vlock.Lock()
defer vlock.Unlock()
for ip, votes := range voteCollection { for ip, votes := range voteCollection {
if votes > max { if votes > max {
max, externalIP = votes, ip max, externalIP = votes, ip
@ -158,3 +144,14 @@ func (c *Consensus) ExternalIP() (net.IP, error) {
// we know it cannot be nil and is valid // we know it cannot be nil and is valid
return net.ParseIP(externalIP), nil return net.ParseIP(externalIP), nil
} }
// waitWG, waits for a waiting group,
// transformed into a channel to be composable.
func waitWG(wg *sync.WaitGroup) chan struct{} {
ch := make(chan struct{})
go func() {
wg.Wait()
close(ch)
}()
return ch
}

View File

@ -5,26 +5,21 @@ import (
"net" "net"
"net/http" "net/http"
"strings" "strings"
"time"
) )
// NewHTTPSource creates a HTTP Source object, // NewHTTPSource creates a HTTP Source object,
// which can be used to request the (external) IP from. // which can be used to request the (external) IP from.
// The Default HTTP Client will be used if no client is given. // The Default HTTP Client will be used if no client is given.
func NewHTTPSource(client *http.Client, url string) *HTTPSource { func NewHTTPSource(url string) *HTTPSource {
if client == nil {
client = http.DefaultClient
}
return &HTTPSource{ return &HTTPSource{
client: client, url: url,
url: url,
} }
} }
// HTTPSource is the default source, to get the external IP from. // HTTPSource is the default source, to get the external IP from.
// It does so by requesting the IP from a URL, via an HTTP GET Request. // It does so by requesting the IP from a URL, via an HTTP GET Request.
type HTTPSource struct { type HTTPSource struct {
client *http.Client
url string url string
parser ContentParser parser ContentParser
} }
@ -42,7 +37,7 @@ func (s *HTTPSource) WithParser(parser ContentParser) *HTTPSource {
} }
// IP implements Source.IP // IP implements Source.IP
func (s *HTTPSource) IP() (net.IP, error) { func (s *HTTPSource) IP(timeout time.Duration) (net.IP, error) {
// Define the GET method with the correct url, // Define the GET method with the correct url,
// setting the User-Agent to our library // setting the User-Agent to our library
req, err := http.NewRequest("GET", s.url, nil) req, err := http.NewRequest("GET", s.url, nil)
@ -51,8 +46,9 @@ func (s *HTTPSource) IP() (net.IP, error) {
} }
req.Header.Set("User-Agent", "go-external-ip (github.com/glendc/go-external-ip)") req.Header.Set("User-Agent", "go-external-ip (github.com/glendc/go-external-ip)")
client := &http.Client{Timeout: timeout}
// Do the request and read the body for non-error results. // Do the request and read the body for non-error results.
resp, err := s.client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -1,15 +1,17 @@
package externalip package externalip
import "net" import (
"net"
"time"
)
// Source defines the part of a voter which gives the actual voting value (IP). // Source defines the part of a voter which gives the actual voting value (IP).
type Source interface { type Source interface {
// IP returns IPv4/IPv6 address in a non-error case // IP returns IPv4/IPv6 address in a non-error case
// net.IP should never be <nil> when error is <nil> // net.IP should never be <nil> when error is <nil>
// NOTE: it is important that IP doesn't block indefinitely, // It is recommended that the IP function times out,
// as the entire Consensus Logic will be blocked indefinitely as well // if no result could be found, after the given timeout duration.
// if this happens. IP(timeout time.Duration) (net.IP, error)
IP() (net.IP, error)
} }
// voter adds weight to the IP given by a source. // voter adds weight to the IP given by a source.
@ -18,13 +20,3 @@ type voter struct {
source Source // provides the IP (see: vote) source Source // provides the IP (see: vote)
weight uint // provides the weight of its vote (acts as a multiplier) weight uint // provides the weight of its vote (acts as a multiplier)
} }
// vote is given by each voter,
// if the Error is not <nil>, the IP and Count values are ignored,
// and the vote has no effect.
// The IP value should never be <nil>, when Error is <nil> as well.
type vote struct {
IP net.IP // the IP proposed by the Voter in question
Count uint // equal to the Voter's weight
Error error // defines if the Vote was cast succesfully or not
}