add initial code

This commit is contained in:
decauwsemaecker.glen@gmail.com 2017-04-05 22:12:07 -05:00
commit c11cf49739
8 changed files with 299 additions and 0 deletions

17
.gitignore vendored Normal file
View File

@ -0,0 +1,17 @@
# Go
# Binaries for programs and plugins
*.exe
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/

19
LICENSE.txt Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2017 Glen De Cauwsemaecker
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

8
README.md Normal file
View File

@ -0,0 +1,8 @@
# Go External IP
TODO:
+ Decent Logging;
+ Comments;
+ Unit-Tests;
+ Iteration & Improved Design;

125
consensus.go Normal file
View File

@ -0,0 +1,125 @@
package externalip
import (
"net"
"net/http"
"time"
)
func DefaultConsensusConfig() *ConsensusConfig {
return &ConsensusConfig{
Timeout: time.Second * 5,
}
}
func DefaultConsensus(cfg *ConsensusConfig) *Consensus {
consensus := NewConsensus(cfg)
// TLS-protected providers
consensus.AddHTTPVoter("https://icanhazip.com/", 3)
consensus.AddHTTPVoter("https://myexternalip.com/raw", 3)
// Plain-text providers
consensus.AddHTTPVoter("http://ifconfig.io/ip", 1)
consensus.AddHTTPVoter("http://checkip.amazonaws.com/", 1)
consensus.AddHTTPVoter("http://ident.me/", 1)
consensus.AddHTTPVoter("http://whatismyip.akamai.com/", 1)
consensus.AddHTTPVoter("http://tnx.nl/ip", 1)
consensus.AddHTTPVoter("http://myip.dnsomatic.com/", 1)
consensus.AddHTTPVoter("http://ipecho.net/plain", 1)
consensus.AddHTTPVoter("http://diagnostic.opendns.com/myip", 1)
return consensus
}
func NewConsensus(cfg *ConsensusConfig) *Consensus {
if cfg == nil {
cfg = DefaultConsensusConfig()
}
return &Consensus{
client: &http.Client{Timeout: cfg.Timeout},
}
}
type ConsensusConfig struct {
Timeout time.Duration
}
func (cfg *ConsensusConfig) WithTimout(timeout time.Duration) *ConsensusConfig {
cfg.Timeout = timeout
return cfg
}
type Consensus struct {
voters []Voter
client *http.Client
}
func (c *Consensus) AddVoter(source Source, weight uint) error {
if source == nil {
return NoSourceError
}
if weight == 0 {
return InsufficientWeightError
}
c.voters = append(c.voters, Voter{
source: source,
weight: weight,
})
return nil
}
func (c *Consensus) AddHTTPVoter(url string, weight uint) error {
return c.AddVoter(NewHTTPSource(c.client, url), weight)
}
func (c *Consensus) AddComplexHTTPVoter(url string, parser ContentParser, weight uint) error {
return c.AddVoter(
NewHTTPSource(c.client, url).WithParser(parser),
weight,
)
}
func (c *Consensus) ExternalIP() (net.IP, error) {
voteCollection := make(map[string]uint)
ch := make(chan Vote, len(c.voters))
for _, voter := range c.voters {
go func(voter Voter) {
ip, err := voter.source.IP()
ch <- Vote{
IP: ip,
Count: voter.weight,
Error: err,
}
}(voter)
}
var count int
for count < len(c.voters) {
select {
case vote := <-ch:
count++
if vote.Error == nil {
voteCollection[vote.IP.String()] += vote.Count
continue
}
}
}
if len(voteCollection) == 0 {
return nil, NoIPError
}
var max uint
var externalIP string
for ip, votes := range voteCollection {
if votes > max {
max, externalIP = votes, ip
}
}
return net.ParseIP(externalIP), nil
}

30
consensus_test.go Normal file
View File

@ -0,0 +1,30 @@
package externalip
import (
"fmt"
"testing"
)
func TestDefaultConsensus(t *testing.T) {
consensus := DefaultConsensus(nil)
if consensus == nil {
t.Fatal("default consensus should never be nil")
}
ip, err := consensus.ExternalIP()
if err != nil {
t.Fatal("couldn't get external IP", err)
}
fmt.Println(ip)
for i := 0; i < 2; i++ {
ipAgain, err := consensus.ExternalIP()
if err != nil {
t.Fatal("couldn't get external IP", err)
}
if !ip.Equal(ipAgain) {
t.Fatalf("expected %q, while received %q", ip, ipAgain)
}
}
}

15
error.go Normal file
View File

@ -0,0 +1,15 @@
package externalip
import "errors"
type InvalidIPError string
func (err InvalidIPError) Error() string {
return "Invalid IP: " + string(err)
}
var (
NoIPError = errors.New("no IP could be found")
InsufficientWeightError = errors.New("a voter's weight has to be at least 1")
NoSourceError = errors.New("no voter's source given")
)

66
sources.go Normal file
View File

@ -0,0 +1,66 @@
package externalip
import (
"io/ioutil"
"net"
"net/http"
"strings"
)
type HTTPSource struct {
client *http.Client
url string
parser ContentParser
}
type ContentParser func(string) (string, error)
func NewHTTPSource(client *http.Client, url string) *HTTPSource {
if client == nil {
client = http.DefaultClient
}
return &HTTPSource{
client: client,
url: url,
}
}
func (s *HTTPSource) WithParser(parser ContentParser) *HTTPSource {
s.parser = parser
return s
}
func (s *HTTPSource) IP() (net.IP, error) {
req, err := http.NewRequest("GET", s.url, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", "go-external-ip (github.com/glendc/go-external-ip)")
resp, err := s.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
bytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
raw := string(bytes)
if s.parser != nil {
raw, err = s.parser(raw)
if err != nil {
return nil, err
}
}
externalIP := net.ParseIP(strings.TrimSpace(raw))
if externalIP == nil {
return nil, InvalidIPError(raw)
}
return externalIP, nil
}

19
types.go Normal file
View File

@ -0,0 +1,19 @@
package externalip
import "net"
type Source interface {
// IP returns IPv4/IPv6 address in a non-error case
IP() (net.IP, error)
}
type Voter struct {
source Source // provides the IP (see: vote)
weight uint // provides the weight of its vote (acts as a multiplier)
}
type Vote struct {
IP net.IP
Count uint
Error error
}