From 9363450cdc6b740a8f78f6932d7d376f0a32f4b0 Mon Sep 17 00:00:00 2001 From: Timmy Welch Date: Thu, 7 Jul 2022 18:40:35 -0700 Subject: [PATCH] v0.0.1 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 --- .github/ISSUE_TEMPLATE.md | 2 +- .gitignore | 1 - .pre-commit-config.yaml | 18 ++++++++ LICENSE.txt | 2 +- cmd/exip/main.go | 2 +- consensus.go | 42 ++++++++++++------ consensus_test.go | 91 ++++++++++++++++++++++++++++++++------- go.mod | 11 +---- go.sum | 21 --------- sources.go | 61 +++++++++++++++++++++----- sources_test.go | 81 ++++++++++++++++++++++++++++++++++ types.go | 1 + 12 files changed, 261 insertions(+), 72 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 sources_test.go diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 034cbc9..752c0cc 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -6,6 +6,6 @@ Please remove the sections that don't apply ## Environment -go-external-ip commit: +go-external-ip commit: go version: OS: diff --git a/.gitignore b/.gitignore index f697170..c1e4b1d 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,3 @@ # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 .glide/ - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..43ef00a --- /dev/null +++ b/.pre-commit-config.yaml @@ -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 diff --git a/LICENSE.txt b/LICENSE.txt index 75112f2..67faff0 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -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. \ No newline at end of file +THE SOFTWARE. diff --git a/cmd/exip/main.go b/cmd/exip/main.go index 6f32c23..2485367 100644 --- a/cmd/exip/main.go +++ b/cmd/exip/main.go @@ -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 diff --git a/consensus.go b/consensus.go index e61ceab..b002a7d 100644 --- a/consensus.go +++ b/consensus.go @@ -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 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, diff --git a/consensus_test.go b/consensus_test.go index 02bd2b8..b10f102 100644 --- a/consensus_test.go +++ b/consensus_test.go @@ -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) } } diff --git a/go.mod b/go.mod index f1a47fd..a9ce2da 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 803f71c..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/sources.go b/sources.go index 370417d..6acf4f1 100644 --- a/sources.go +++ b/sources.go @@ -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 +} diff --git a/sources_test.go b/sources_test.go new file mode 100644 index 0000000..9fb7258 --- /dev/null +++ b/sources_test.go @@ -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) + } +} diff --git a/types.go b/types.go index 1cf9c57..1a37944 100644 --- a/types.go +++ b/types.go @@ -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.