Add pre-commit config
Update default consensus with more sources
Move IP version spec to Source so that dual-stack sites can be used
Add tests for HTTPSource and consensus
This commit is contained in:
Timmy Welch 2022-07-07 18:40:35 -07:00
parent d8b5497932
commit 9363450cdc
12 changed files with 261 additions and 72 deletions

View File

@ -6,6 +6,6 @@ Please remove the sections that don't apply
## Environment
go-external-ip commit:
go-external-ip commit:
go version:
OS:

1
.gitignore vendored
View File

@ -14,4 +14,3 @@
# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736
.glide/

18
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,18 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.2.0
hooks:
- id: trailing-whitespace
args: [--markdown-linebreak-ext=.gitignore]
- id: end-of-file-fixer
- id: check-yaml
- repo: https://github.com/tekwizely/pre-commit-golang
rev: v1.0.0-beta.5
hooks:
- id: go-mod-tidy
- id: go-imports
args: [-w]
- repo: https://github.com/golangci/golangci-lint
rev: v1.46.2
hooks:
- id: golangci-lint

View File

@ -16,4 +16,4 @@ 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.
THE SOFTWARE.

View File

@ -34,7 +34,7 @@ func main() {
consensus := externalip.DefaultConsensus(cfg, logger)
// retrieve the external ip
ip4, err := consensus.ExternalIP(4)
ip4, _ := consensus.ExternalIP(4)
ip6, err := consensus.ExternalIP(6)
// simple error handling

View File

@ -27,17 +27,35 @@ func DefaultConsensus(cfg *ConsensusConfig, logger *log.Logger) *Consensus {
consensus := NewConsensus(cfg, logger)
// TLS-protected providers
consensus.AddVoter(NewHTTPSource("https://icanhazip.com/"), 3, IPv6)
consensus.AddVoter(NewHTTPSource("https://myexternalip.com/raw"), 3, IPv4)
_ = consensus.AddVoter(NewHTTPSource("https://myip-mnpz5ateaq-uw.a.run.app", IPv4), 3)
_ = consensus.AddVoter(NewHTTPSource("https://myip-mnpz5ateaq-uw.a.run.app", IPv6), 3)
_ = consensus.AddVoter(NewHTTPSource("https://api.my-ip.io/ip", IPv4), 1)
_ = consensus.AddVoter(NewHTTPSource("https://api.my-ip.io/ip", IPv6), 1)
_ = consensus.AddVoter(NewHTTPSource("https://wtfismyip.com/text", IPv4), 1)
_ = consensus.AddVoter(NewHTTPSource("https://wtfismyip.com/text", IPv6), 1)
_ = consensus.AddVoter(NewHTTPSource("https://icanhazip.com", IPv4), 1)
_ = consensus.AddVoter(NewHTTPSource("https://icanhazip.com", IPv6), 1)
_ = consensus.AddVoter(NewHTTPSource("https://api.ip.sb/ip", IPv4), 1)
_ = consensus.AddVoter(NewHTTPSource("https://api.ip.sb/ip", IPv6), 1)
_ = consensus.AddVoter(NewHTTPSource("https://api.myip.la", IPv4), 1)
_ = consensus.AddVoter(NewHTTPSource("https://api.myip.la", IPv6), 1)
_ = consensus.AddVoter(NewHTTPSource("https://ident.me", IPv4), 1)
_ = consensus.AddVoter(NewHTTPSource("https://ident.me", IPv6), 1)
_ = consensus.AddVoter(NewHTTPSource("https://api.ipify.org/?format=plain", IPv4), 1)
_ = consensus.AddVoter(NewHTTPSource("http://api.ipaddress.com/myip", IPv4), 1)
_ = consensus.AddVoter(NewHTTPSource("https://ipecho.net/plain", IPv4), 1)
// Plain-text providers
consensus.AddVoter(NewHTTPSource("http://ifconfig.io/ip"), 1, IPv6)
consensus.AddVoter(NewHTTPSource("http://checkip.amazonaws.com/"), 1, IPv4)
consensus.AddVoter(NewHTTPSource("http://ident.me/"), 1, IPv6)
consensus.AddVoter(NewHTTPSource("http://whatismyip.akamai.com/"), 1, IPv4)
consensus.AddVoter(NewHTTPSource("http://tnx.nl/ip"), 1, IPv6)
consensus.AddVoter(NewHTTPSource("http://myip.dnsomatic.com/"), 1, IPv4)
consensus.AddVoter(NewHTTPSource("http://diagnostic.opendns.com/myip"), 1, IPv6)
_ = consensus.AddVoter(NewHTTPSource("https://ifconfig.io/ip", IPv6), 1)
_ = consensus.AddVoter(NewHTTPSource("https://ifconfig.io/ip", IPv4), 1)
_ = consensus.AddVoter(NewHTTPSource("https://checkip.amazonaws.com/", IPv4), 1)
_ = consensus.AddVoter(NewHTTPSource("http://whatismyip.akamai.com/", IPv4), 1)
_ = consensus.AddVoter(NewHTTPSource("https://tnx.nl/", IPv6), 1)
_ = consensus.AddVoter(NewHTTPSource("https://tnx.nl/", IPv4), 1)
_ = consensus.AddVoter(NewHTTPSource("http://myip.dnsomatic.com/", IPv4), 1)
_ = consensus.AddVoter(NewHTTPSource("https://diagnostic.opendns.com/myip", IPv6), 1)
return consensus
}
@ -83,7 +101,7 @@ type Consensus struct {
// 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, ipversion uint) error {
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
@ -92,13 +110,13 @@ func (c *Consensus) AddVoter(source Source, weight, ipversion uint) error {
c.logger.Println("[ERROR] could not add voter: weight cannot be 0")
return ErrInsufficientWeight
}
if ipversion == 4 {
if source.IPVersion() == 4 {
c.voters4 = append(c.voters4, voter{
source: source,
weight: weight,
})
}
if ipversion == 6 {
if source.IPVersion() == 6 {
c.voters6 = append(c.voters6, voter{
source: source,
weight: weight,

View File

@ -1,30 +1,91 @@
package externalip
package externalip_test
import (
"fmt"
"net"
"net/http"
"net/http/httptest"
"testing"
"time"
"git.narnian.us/lordwelch/externalip"
)
func TestDefaultConsensus(t *testing.T) {
consensus := DefaultConsensus(nil)
func newConcensus(url string) *externalip.Consensus {
consensus := externalip.NewConsensus(&externalip.ConsensusConfig{
Timeout: time.Second * 30,
}, nil)
// TLS-protected providers
_ = consensus.AddVoter(externalip.NewHTTPSource(url, externalip.IPv4), 1)
_ = consensus.AddVoter(externalip.NewHTTPSource(url, externalip.IPv4), 1)
_ = consensus.AddVoter(externalip.NewHTTPSource(url, externalip.IPv4), 1)
return consensus
}
func newServer(ip1, ip2, ip3 string) *httptest.Server {
var try = 1
return httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
switch try {
case 1:
fmt.Fprintln(w, ip1)
case 2:
fmt.Fprintln(w, ip2)
case 3:
fmt.Fprintln(w, ip3)
try = 1
}
try++
}))
}
func TestConsensus(t *testing.T) {
server := newServer("127.0.0.1", "127.0.0.1", "127.0.0.2")
consensus := newConcensus(server.URL)
if consensus == nil {
t.Fatal("default consensus should never be nil")
}
ip, err := consensus.ExternalIP()
ip, err := consensus.ExternalIP(4)
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)
}
if !ip.Equal(net.IPv4(127, 0, 0, 1)) {
t.Errorf("invalid ip found: expected %s recieved %s", "127.0.0.1", ip)
}
}
func TestConsensus2(t *testing.T) {
server := newServer("127.0.0.1", "127.0.0.2", "127.0.0.2")
consensus := newConcensus(server.URL)
if consensus == nil {
t.Fatal("default consensus should never be nil")
}
ip, err := consensus.ExternalIP(4)
if err != nil {
t.Fatal("couldn't get external IP", err)
}
if !ip.Equal(net.IPv4(127, 0, 0, 2)) {
t.Errorf("invalid ip found: expected %s recieved %s", "127.0.0.2", ip)
}
}
func TestConsensus3(t *testing.T) {
server := newServer("127.0.0.1", "127.0.0.2", "127.0.0.3")
consensus := newConcensus(server.URL)
if consensus == nil {
t.Fatal("default consensus should never be nil")
}
ip, err := consensus.ExternalIP(4)
if err != nil {
t.Fatal("couldn't get external IP", err)
}
if !ip.Equal(net.IPv4(127, 0, 0, 1)) {
t.Errorf("invalid ip found: expected %s recieved %s; This will fail on multiple runs it takes whichever request returns first", "127.0.0.1", ip)
}
}

11
go.mod
View File

@ -1,12 +1,3 @@
module git.narnian.us/lordwelch/externalip
go 1.12
require (
github.com/google/pprof v0.0.0-20190908185732-236ed259b199 // indirect
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 // indirect
golang.org/x/arch v0.0.0-20190919213554-7fe50f7625bd // indirect
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 // indirect
golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13 // indirect
golang.org/x/tools v0.0.0-20190919223014-db1d4edb4685 // indirect
)
go 1.18

21
go.sum
View File

@ -1,21 +0,0 @@
github.com/google/pprof v0.0.0-20190908185732-236ed259b199 h1:sEyCq3pOT7tNC+3gcLI7sZkBDgntZ6wQJNmr9lmIjIc=
github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 h1:UDMh68UUwekSh5iP2OMhRRZJiiBccgV7axzUG8vi56c=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
golang.org/x/arch v0.0.0-20190919213554-7fe50f7625bd h1:IbZRdF+nCjC31g8APRj0sBwNe35VSPHNThzJKA0idTs=
golang.org/x/arch v0.0.0-20190919213554-7fe50f7625bd/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7 h1:0hQKqeLdqlt5iIwVOBErRisrHJAN57yOiPRQItI20fU=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13 h1:/zi0zzlPHWXYXrO1LjNRByFu8sdGgCkj2JLDdBIB84k=
golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190919223014-db1d4edb4685 h1:/6Ol4IqB+r3aIk191dJQFcnPHMW+pj8RzXAz3ddkmk4=
golang.org/x/tools v0.0.0-20190919223014-db1d4edb4685/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@ -1,6 +1,7 @@
package externalip
import (
"context"
"io/ioutil"
"log"
"net"
@ -12,17 +13,19 @@ import (
// NewHTTPSource creates a HTTP Source object,
// which can be used to request the (external) IP from.
// The Default HTTP Client will be used if no client is given.
func NewHTTPSource(url string) *HTTPSource {
func NewHTTPSource(url string, ipversion uint) *HTTPSource {
return &HTTPSource{
url: url,
url: url,
ipversion: ipversion,
}
}
// 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.
type HTTPSource struct {
url string
parser ContentParser
url string
parser ContentParser
ipversion uint
}
// ContentParser can be used to add a parser to an HTTPSource
@ -39,27 +42,57 @@ func (s *HTTPSource) WithParser(parser ContentParser) *HTTPSource {
// IP implements Source.IP
func (s *HTTPSource) IP(timeout time.Duration, logger *log.Logger) (net.IP, error) {
var (
dialer = &net.Dialer{
Timeout: 30 * time.Second,
DualStack: false,
FallbackDelay: -1,
KeepAlive: 30 * time.Second,
}
n = "tcp4"
)
if s.ipversion == 6 {
n = "tcp6"
}
// Define the GET method with the correct url,
// setting the User-Agent to our library
req, err := http.NewRequest("GET", s.url, nil)
if err != nil {
logger.Printf("[ERROR] could not create a GET Request for %q: %v\n", s.url, err)
if logger != nil {
logger.Printf("[ERROR] could not create a GET Request for %q: %v\n", s.url, err)
}
return nil, err
}
req.Header.Set("User-Agent", "go-external-ip (github.com/glendc/go-external-ip)")
client := &http.Client{Timeout: timeout}
client := &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network string, addr string) (net.Conn, error) {
return dialer.DialContext(ctx, n, addr)
},
TLSHandshakeTimeout: 10 * time.Second,
MaxIdleConns: 100,
IdleConnTimeout: 30 * time.Second,
ResponseHeaderTimeout: timeout,
ExpectContinueTimeout: 1 * time.Second,
},
Timeout: timeout,
}
// Do the request and read the body for non-error results.
resp, err := client.Do(req)
if err != nil {
logger.Printf("[ERROR] could not GET %q: %v\n", s.url, err)
if logger != nil {
logger.Printf("[ERROR] could not GET %q: %v\n", s.url, err)
}
return nil, err
}
defer resp.Body.Close()
bytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
logger.Printf("[ERROR] could not read response from %q: %v\n", s.url, err)
if logger != nil {
logger.Printf("[ERROR] could not read response from %q: %v\n", s.url, err)
}
return nil, err
}
@ -68,7 +101,9 @@ func (s *HTTPSource) IP(timeout time.Duration, logger *log.Logger) (net.IP, erro
if s.parser != nil {
raw, err = s.parser(raw)
if err != nil {
logger.Printf("[ERROR] could not parse response from %q: %v\n", s.url, err)
if logger != nil {
logger.Printf("[ERROR] could not parse response from %q: %v\n", s.url, err)
}
return nil, err
}
}
@ -76,10 +111,16 @@ func (s *HTTPSource) IP(timeout time.Duration, logger *log.Logger) (net.IP, erro
// validate the IP
externalIP := net.ParseIP(strings.TrimSpace(raw))
if externalIP == nil {
logger.Printf("[ERROR] %q returned an invalid IP: %v\n", s.url, err)
if logger != nil {
logger.Printf("[ERROR] %q returned an invalid IP: %v\n", s.url, err)
}
return nil, InvalidIPError(raw)
}
// returned the parsed IP
return externalIP, nil
}
func (s *HTTPSource) IPVersion() uint {
return s.ipversion
}

81
sources_test.go Normal file
View File

@ -0,0 +1,81 @@
package externalip_test
import (
"fmt"
"net"
"net/http"
"net/http/httptest"
"testing"
"time"
"git.narnian.us/lordwelch/externalip"
)
func TestHTTPSource(t *testing.T) {
server := httptest.NewUnstartedServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
fmt.Fprintln(w, r.RemoteAddr[1:len(r.RemoteAddr)-7])
}))
server.Listener.Close()
server.Listener, _ = net.Listen("tcp6", "[::1]:0")
server.Start()
defer server.Close()
source := externalip.NewHTTPSource("http://"+server.Listener.Addr().String(), 6)
ip, _ := source.IP(time.Second*10, nil)
if ip.String() != "::1" {
t.Errorf("invalid ip found: expected %s recieved %s", "::1", ip)
}
}
func TestHTTPSource2(t *testing.T) {
server := httptest.NewUnstartedServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
fmt.Fprintln(w, r.RemoteAddr[1:len(r.RemoteAddr)-7])
}))
server.Listener.Close()
server.Listener, _ = net.Listen("tcp6", "[::1]:0")
server.Start()
defer server.Close()
source := externalip.NewHTTPSource("http://"+server.Listener.Addr().String(), 4)
ip, _ := source.IP(time.Second*10, nil)
if ip != nil {
t.Errorf("invalid ip found: expected %s recieved %s", "nil", ip)
}
}
func TestHTTPSource3(t *testing.T) {
server := httptest.NewUnstartedServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
fmt.Fprintln(w, r.RemoteAddr[:len(r.RemoteAddr)-6])
}))
server.Listener.Close()
server.Listener, _ = net.Listen("tcp4", "127.0.0.1:0")
server.Start()
defer server.Close()
source := externalip.NewHTTPSource("http://"+server.Listener.Addr().String(), 4)
ip, _ := source.IP(time.Second*10, nil)
if ip.String() != "127.0.0.1" {
t.Errorf("invalid ip found: expected %s recieved %s", "127.0.0.1", ip)
}
}
func TestHTTPSource4(t *testing.T) {
server := httptest.NewUnstartedServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
fmt.Fprintln(w, r.RemoteAddr[:len(r.RemoteAddr)-6])
}))
server.Listener.Close()
server.Listener, _ = net.Listen("tcp4", "127.0.0.1:0")
server.Start()
defer server.Close()
source := externalip.NewHTTPSource("http://"+server.Listener.Addr().String(), 6)
ip, _ := source.IP(time.Second*10, nil)
if ip != nil {
t.Errorf("invalid ip found: expected %s recieved %s", "nil", ip)
}
}

View File

@ -13,6 +13,7 @@ type Source interface {
// It is recommended that the IP function times out,
// if no result could be found, after the given timeout duration.
IP(timeout time.Duration, logger *log.Logger) (net.IP, error)
IPVersion() uint
}
// voter adds weight to the IP given by a source.