diff --git a/dot/network/host.go b/dot/network/host.go index ea8ecb533a..a7ef2c0096 100644 --- a/dot/network/host.go +++ b/dot/network/host.go @@ -15,7 +15,7 @@ import ( "time" "github.com/ChainSafe/gossamer/dot/peerset" - "github.com/chyeh/pubip" + "github.com/ChainSafe/gossamer/internal/pubip" "github.com/dgraph-io/ristretto" badger "github.com/ipfs/go-ds-badger2" "github.com/libp2p/go-libp2p" diff --git a/go.mod b/go.mod index ba317ded90..954d3de43a 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ require ( github.com/btcsuite/btcd/btcutil v1.1.5 github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce github.com/centrifuge/go-substrate-rpc-client/v4 v4.1.0 - github.com/chyeh/pubip v0.0.0-20170203095919-b7e679cf541c github.com/cockroachdb/pebble v1.0.0 github.com/cosmos/go-bip39 v1.0.0 github.com/dgraph-io/badger/v2 v2.2007.4 @@ -23,6 +22,7 @@ require ( github.com/gorilla/websocket v1.5.1 github.com/gtank/merlin v0.1.1 github.com/ipfs/go-ds-badger2 v0.1.3 + github.com/jpillora/backoff v1.0.0 github.com/jpillora/ipfilter v1.2.9 github.com/klauspost/compress v1.17.5 github.com/libp2p/go-libp2p v0.31.0 @@ -106,7 +106,6 @@ require ( github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect github.com/jbenet/goprocess v0.1.4 // indirect github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 // indirect - github.com/jpillora/backoff v1.0.0 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/koron/go-ssdp v0.0.4 // indirect github.com/kr/pretty v0.3.1 // indirect diff --git a/go.sum b/go.sum index 8652b73046..5dc78a75b9 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,6 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chyeh/pubip v0.0.0-20170203095919-b7e679cf541c h1:++BhWlmSX+n8m3O4gPfy3S4PTZ0TMzH6nelerBLPUng= -github.com/chyeh/pubip v0.0.0-20170203095919-b7e679cf541c/go.mod h1:C7ma6h458jTWT65mXC58L1Q6hnEtr0unur8cMc0UEXM= github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= diff --git a/internal/pubip/pubip.go b/internal/pubip/pubip.go new file mode 100644 index 0000000000..5f04331f0f --- /dev/null +++ b/internal/pubip/pubip.go @@ -0,0 +1,137 @@ +// Copyright 2024 ChainSafe Systems (ON) +// SPDX-License-Identifier: LGPL-3.0-only + +package pubip + +import ( + "errors" + "fmt" + "io" + "net" + "net/http" + "reflect" + "strconv" + "strings" + "time" + + "github.com/jpillora/backoff" +) + +// MaxTries is the maximum amount of tries to attempt to one service. +const MaxTries = 3 + +// APIURIs is the URIs of the services. +var APIURIs = []string{ + "https://api.ipify.org", + "http://ipinfo.io/ip", + "http://checkip.amazonaws.com", + "http://whatismyip.akamai.com", + "http://ipv4.text.wtfismyip.com", +} + +// Timeout sets the time limit of collecting results from different services. +var Timeout = 2 * time.Second + +// GetIPBy queries an API to retrieve a `net.IP` of this machine's public IP +// address. +func GetIPBy(dest string) (net.IP, error) { + b := &backoff.Backoff{ + Jitter: true, + } + client := &http.Client{} + + req, err := http.NewRequest(http.MethodGet, dest, nil) + if err != nil { + return nil, err + } + + for tries := 0; tries < MaxTries; tries++ { + resp, err := client.Do(req) + if err != nil { + d := b.Duration() + time.Sleep(d) + continue + } + + defer resp.Body.Close() //nolint + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, errors.New(dest + " status code " + strconv.Itoa(resp.StatusCode) + ", body: " + string(body)) + } + + tb := strings.TrimSpace(string(body)) + ip := net.ParseIP(tb) + if ip == nil { + return nil, errors.New("IP address not valid: " + tb) + } + return ip, nil + } + + return nil, errors.New("Failed to reach " + dest) +} + +func detailErr(err error, errs []error) error { + errStrs := []string{err.Error()} + for _, e := range errs { + errStrs = append(errStrs, e.Error()) + } + j := strings.Join(errStrs, "\n") + return errors.New(j) +} + +func validate(rs []net.IP) (net.IP, error) { + if rs == nil { + return nil, fmt.Errorf("failed to get any result from %d APIs", len(APIURIs)) + } + if len(rs) < 3 { + return nil, fmt.Errorf("less than %d results from %d APIs", 3, len(APIURIs)) + } + first := rs[0] + for i := 1; i < len(rs); i++ { + if !reflect.DeepEqual(first, rs[i]) { + return nil, fmt.Errorf("results are not identical: %s", rs) + } + } + return first, nil +} + +func worker(d string, r chan<- net.IP, e chan<- error) { + ip, err := GetIPBy(d) + if err != nil { + e <- err + return + } + r <- ip +} + +// Get queries several APIs to retrieve a `net.IP` of this machine's public IP +// address. +func Get() (net.IP, error) { + var results []net.IP + resultCh := make(chan net.IP, len(APIURIs)) + var errs []error + errCh := make(chan error, len(APIURIs)) + + for _, d := range APIURIs { + go worker(d, resultCh, errCh) + } + for { + select { + case err := <-errCh: + errs = append(errs, err) + case r := <-resultCh: + results = append(results, r) + case <-time.After(Timeout): + r, err := validate(results) + if err != nil { + return nil, detailErr(err, errs) + } + return r, nil + } + } +} diff --git a/internal/pubip/pubip_test.go b/internal/pubip/pubip_test.go new file mode 100644 index 0000000000..cbbc0925c9 --- /dev/null +++ b/internal/pubip/pubip_test.go @@ -0,0 +1,35 @@ +// Copyright 2024 ChainSafe Systems (ON) +// SPDX-License-Identifier: LGPL-3.0-only + +package pubip + +import ( + "net" + "reflect" + "testing" +) + +func TestIsValidate(t *testing.T) { + tests := []struct { + input []net.IP + expected net.IP + }{ + {nil, nil}, + {[]net.IP{}, nil}, + {[]net.IP{net.ParseIP("192.168.1.1")}, nil}, + {[]net.IP{net.ParseIP("192.168.1.1"), net.ParseIP("192.168.1.1")}, nil}, + {[]net.IP{net.ParseIP("192.168.1.1"), net.ParseIP("192.168.1.1"), net.ParseIP("192.168.1.1")}, + net.ParseIP("192.168.1.1")}, + {[]net.IP{net.ParseIP("192.168.1.1"), net.ParseIP("192.168.1.2")}, nil}, + {[]net.IP{net.ParseIP("192.168.1.1"), net.ParseIP("192.168.1.1"), net.ParseIP("192.168.1.2")}, + nil}, + } + for i, v := range tests { + actual, _ := validate(v.input) + expected := v.expected + t.Logf("Check case %d: %s(actual) == %s(expected)", i, actual, expected) + if !reflect.DeepEqual(actual, expected) { + t.Errorf("Error on case %d: %s(actual) != %s(expected)", i, actual, expected) + } + } +}