add initial code
This commit is contained in:
commit
c11cf49739
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal 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
19
LICENSE.txt
Normal 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
8
README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Go External IP
|
||||
|
||||
TODO:
|
||||
|
||||
+ Decent Logging;
|
||||
+ Comments;
|
||||
+ Unit-Tests;
|
||||
+ Iteration & Improved Design;
|
125
consensus.go
Normal file
125
consensus.go
Normal 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
30
consensus_test.go
Normal 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
15
error.go
Normal 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
66
sources.go
Normal 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
19
types.go
Normal 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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user