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