pax_global_header00006660000000000000000000000064141332555440014520gustar00rootroot0000000000000052 comment=2f10cb29b5c037919a65563c184fb64fcd13f8bb go-external-ip-0.1.0/000077500000000000000000000000001413325554400143515ustar00rootroot00000000000000go-external-ip-0.1.0/.github/000077500000000000000000000000001413325554400157115ustar00rootroot00000000000000go-external-ip-0.1.0/.github/ISSUE_TEMPLATE.md000066400000000000000000000002221413325554400204120ustar00rootroot00000000000000## Problem statement Please remove the sections that don't apply ## Steps to reproduce ## Environment go-external-ip commit: go version: OS: go-external-ip-0.1.0/.github/workflows/000077500000000000000000000000001413325554400177465ustar00rootroot00000000000000go-external-ip-0.1.0/.github/workflows/go.yml000066400000000000000000000011731413325554400211000ustar00rootroot00000000000000name: Go on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build-min: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.13 - name: Build run: go build -v ./... - name: Test run: go test -v ./... build-max: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.17 - name: Build run: go build -v ./... - name: Test run: go test -v ./... go-external-ip-0.1.0/.gitignore000066400000000000000000000004421413325554400163410ustar00rootroot00000000000000# 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/ go.lock go-external-ip-0.1.0/CHANGES.md000066400000000000000000000014371413325554400157500ustar00rootroot00000000000000 # v0.1.0 (2021-10-18) - Add `(*Consensus).UseIPProtocol` method to restrict the IP returned to be able to either IPv4 or IPv6; - Author: [Jamie Curnow, @jc21](https://github.com/jc21); - Contributors: [Tejas](https://github.com/Tejas); # v0.0.1 (untagged) - remove `tnx.nl` from default Consensus source; - Author: [Juerd Waalboer, @Juerd](https://github.com/Juerd); # v0.0.0 (untagged) - initial release of this Go package; - contains the `Consensus API` which in most cases is used using the default configuration; - custom configurations are supported, allowing you to be able to define your own sources; - shipped together with a small binary `exip` tool which allows you to quickly get our IP address; Author: [Glen De Cauwsemaecker, @glendc](https://github.com/glendc) go-external-ip-0.1.0/LICENSE.txt000066400000000000000000000020501413325554400161710ustar00rootroot00000000000000Copyright (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.go-external-ip-0.1.0/README.md000066400000000000000000000043171413325554400156350ustar00rootroot00000000000000# Go External IP [![Go Workflow Status](https://github.com/glendc/go-external-ip/workflows/Go/badge.svg)](https://github.com/glendc/go-external-ip/actions?query=workflow%Go) [![GoDoc](https://godoc.org/github.com/glendc/go-external-ip?status.svg)](https://godoc.org/github.com/glendc/go-external-ip) [![Go Report Card](https://goreportcard.com/badge/github.com/glendc/go-external-ip)](https://goreportcard.com/report/github.com/glendc/go-external-ip)[![license](https://img.shields.io/github/license/glendc/go-external-ip.svg)](https://github.com/GlenDC/go-external-ip/blob/master/LICENSE.txt) A Golang library to get your external ip from multiple services. ## TODO + Design/Implement STUNSource, more info: + [RFC 3489](https://tools.ietf.org/html/rfc3489); + [RFC 5389](https://tools.ietf.org/html/rfc5389); ## Docs https://godoc.org/github.com/GlenDC/go-external-ip ## Usage Using the library can as simple as the following (runnable) example: ```go package main import ( "fmt" "github.com/glendc/go-external-ip" ) func main() { // Create the default consensus, // using the default configuration and no logger. consensus := externalip.DefaultConsensus(nil, nil) // By default Ipv4 or Ipv6 is returned, // use the function below to limit yourself to IPv4, // or pass in `6` instead to limit yourself to IPv6. // consensus.UseIPProtocol(4) // Get your IP, // which is never when err is . ip, err := consensus.ExternalIP() if err == nil { fmt.Println(ip.String()) // print IPv4/IPv6 in string format } } ``` Please read [the documentation][docs] for more information. ## exip This library also comes with a standalone command line application, which can be used to get your external IP, directly from your terminal. ### install ``` $ go install github.com/glendc/go-external-ip/cmd/exip ``` ### usage ``` $ exip -h Retrieve your external IP. Usage: exip [flags] Flags: -h help show this usage message -p uint IP Protocol to be used (0, 4, or 6) -t duration consensus's voting timeout (default 5s) -v log errors to STDERR, when defined ``` [docs]: https://pkg.go.dev/github.com/GlenDC/go-external-ip go-external-ip-0.1.0/cmd/000077500000000000000000000000001413325554400151145ustar00rootroot00000000000000go-external-ip-0.1.0/cmd/exip/000077500000000000000000000000001413325554400160615ustar00rootroot00000000000000go-external-ip-0.1.0/cmd/exip/main.go000066400000000000000000000026431413325554400173410ustar00rootroot00000000000000package main import ( "flag" "fmt" "log" "os" "time" externalip "github.com/glendc/go-external-ip" ) // CLI Flags var ( timeout = flag.Duration("t", time.Second*5, "consensus's voting timeout") verbose = flag.Bool("v", false, "log errors to STDERR, when defined") protocol = flag.Uint("p", 0, "IP Protocol to be used (0, 4, or 6)") ) func main() { // configure the consensus cfg := externalip.DefaultConsensusConfig() if timeout != nil { cfg.WithTimeout(*timeout) } // optionally create the logger, // if no logger is defined, all logs will be discarded. var logger *log.Logger if verbose != nil && *verbose { logger = externalip.NewLogger(os.Stderr) } // create the consensus consensus := externalip.DefaultConsensus(cfg, logger) err := consensus.UseIPProtocol(*protocol) errCheck(err) // retrieve the external ip ip, err := consensus.ExternalIP() errCheck(err) // success, simply output the IP in string format fmt.Println(ip.String()) } func errCheck(err error) { if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func init() { // Define customized usage output flag.Usage = func() { fmt.Fprint(os.Stderr, "Retrieve your external IP.\n\n") fmt.Fprintf(os.Stderr, "Usage:\n %s [flags]\n\n", os.Args[0]) fmt.Fprintln(os.Stderr, "Flags:") fmt.Fprintf(os.Stderr, " -h help\n \tshow this usage message\n") flag.PrintDefaults() } // Parse CLI Flags flag.Parse() } go-external-ip-0.1.0/consensus.go000066400000000000000000000107501413325554400167230ustar00rootroot00000000000000package externalip import ( "log" "net" "sync" "time" ) // DefaultConsensusConfig returns the ConsensusConfig, // with the default values: // + Timeout: 30 seconds; func DefaultConsensusConfig() *ConsensusConfig { return &ConsensusConfig{ Timeout: time.Second * 30, } } // DefaultConsensus returns a consensus filled // with default and recommended HTTPSources. // TLS-Protected providers get more power, // compared to plain-text providers. func DefaultConsensus(cfg *ConsensusConfig, logger *log.Logger) *Consensus { consensus := NewConsensus(cfg, logger) // TLS-protected providers consensus.AddVoter(NewHTTPSource("https://icanhazip.com/"), 3) consensus.AddVoter(NewHTTPSource("https://myexternalip.com/raw"), 3) // Plain-text providers consensus.AddVoter(NewHTTPSource("http://ifconfig.io/ip"), 1) consensus.AddVoter(NewHTTPSource("http://checkip.amazonaws.com/"), 1) consensus.AddVoter(NewHTTPSource("http://ident.me/"), 1) consensus.AddVoter(NewHTTPSource("http://whatismyip.akamai.com/"), 1) consensus.AddVoter(NewHTTPSource("http://myip.dnsomatic.com/"), 1) consensus.AddVoter(NewHTTPSource("http://diagnostic.opendns.com/myip"), 1) return consensus } // NewConsensus creates a new Consensus, with no sources. // When the given cfg is , the `DefaultConsensusConfig` will be used. func NewConsensus(cfg *ConsensusConfig, logger *log.Logger) *Consensus { if cfg == nil { cfg = DefaultConsensusConfig() } if logger == nil { logger = NewLogger(nil) } return &Consensus{ timeout: cfg.Timeout, logger: logger, } } // ConsensusConfig is used to configure the Consensus, while creating it. type ConsensusConfig struct { Timeout time.Duration } // WithTimeout sets the voting timeout of this config, // returning the config itself at the end, to allow for chaining func (cfg *ConsensusConfig) WithTimeout(timeout time.Duration) *ConsensusConfig { cfg.Timeout = timeout return cfg } // Consensus the type at the center of this library, // and is the main entry point for users. // Its `ExternalIP` method allows you to ask for your ExternalIP, // influenced by all its added voters. type Consensus struct { voters []voter timeout time.Duration logger *log.Logger protocol uint } // 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 uint) error { if source == nil { c.logger.Println("[ERROR] could not add voter: no source given") return ErrNoSource } if weight == 0 { c.logger.Println("[ERROR] could not add voter: weight cannot be 0") return ErrInsufficientWeight } c.voters = append(c.voters, voter{ source: source, weight: weight, }) return nil } // ExternalIP requests asynchronously the externalIP from all added voters, // returning the IP which received the most votes. // The returned IP will always be valid, in case the returned error is . func (c *Consensus) ExternalIP() (net.IP, error) { voteCollection := make(map[string]uint) var vlock sync.Mutex var wg sync.WaitGroup // start all source Requests on a seperate goroutine for _, v := range c.voters { wg.Add(1) go func(v voter) { defer wg.Done() ip, err := v.source.IP(c.timeout, c.logger, c.protocol) if err == nil && ip != nil { vlock.Lock() defer vlock.Unlock() voteCollection[ip.String()] += v.weight } }(v) } // wait for all votes to come in, // or until their process times out wg.Wait() // if no votes were casted succesfully, // return early with an error if len(voteCollection) == 0 { c.logger.Println("[ERROR] no votes were casted succesfully") return nil, ErrNoIP } var max uint var externalIP string // find the IP which has received the most votes, // influinced by the voter's weight. vlock.Lock() defer vlock.Unlock() for ip, votes := range voteCollection { if votes > max { max, externalIP = votes, ip } } // as the found IP was parsed previously, // we know it cannot be nil and is valid return net.ParseIP(externalIP), nil } // UseIPProtocol will set the IP Protocol to use for http requests // to the sources. If zero, it will not discriminate. This is useful // when you want to get the external IP in a specific protocol. // Protocol only supports 0, 4 or 6. func (c *Consensus) UseIPProtocol(protocol uint) error { if protocol != 0 && protocol != 4 && protocol != 6 { c.logger.Println("[ERROR] only ipv4 and ipv6 protocol is supported") return ErrInvalidProtocol } c.protocol = protocol return nil } go-external-ip-0.1.0/consensus_test.go000066400000000000000000000010531413325554400177560ustar00rootroot00000000000000package externalip import ( "fmt" "testing" ) func TestDefaultConsensus(t *testing.T) { consensus := DefaultConsensus(nil, 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) } } } go-external-ip-0.1.0/error.go000066400000000000000000000015531413325554400160350ustar00rootroot00000000000000package externalip import "errors" // InvalidIPError is returned when an value returned is invalid. // This error should be returned by the source itself. type InvalidIPError string // Error implements error.Error func (err InvalidIPError) Error() string { return "Invalid IP: " + string(err) } var ( // ErrNoIP is returned by the Consensus when no vote was casted successfully ErrNoIP = errors.New("no IP could be found") // ErrInsufficientWeight is returned when a voter's weight is invalid ErrInsufficientWeight = errors.New("a voter's weight has to be at least 1") // ErrNoSource is returned when a voter is added, // which doesn't have a source specified ErrNoSource = errors.New("no voter's source given") // ErrInvalidProtocol is used when setting an invalid ip protocol on the conensus ErrInvalidProtocol = errors.New("invalid ip protocol specified") ) go-external-ip-0.1.0/go.mod000066400000000000000000000000611413325554400154540ustar00rootroot00000000000000module github.com/glendc/go-external-ip go 1.13 go-external-ip-0.1.0/log.go000066400000000000000000000005141413325554400154610ustar00rootroot00000000000000package externalip import ( "io" "io/ioutil" "log" ) // NewLogger returns a new standard logger, with a given writer. // if w is , all logs will be discarded. func NewLogger(w io.Writer) *log.Logger { if w == nil { w = ioutil.Discard } return log.New(w, "", log.Ldate|log.Ltime|log.Lmicroseconds|log.Lshortfile) } go-external-ip-0.1.0/sources.go000066400000000000000000000057251413325554400163740ustar00rootroot00000000000000package externalip import ( "errors" "io/ioutil" "log" "net" "net/http" "strings" "syscall" "time" ) // 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 { return &HTTPSource{ url: url, } } // 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 } // ContentParser can be used to add a parser to an HTTPSource // to parse the raw content returned from a website, and return the IP. // Spacing before and after the IP will be trimmed by the Consensus. type ContentParser func(string) (string, error) // WithParser sets the parser value as the value to be used by this HTTPSource, // and returns the pointer to this source, to allow for chaining. func (s *HTTPSource) WithParser(parser ContentParser) *HTTPSource { s.parser = parser return s } // IP implements Source.IP func (s *HTTPSource) IP(timeout time.Duration, logger *log.Logger, protocol uint) (net.IP, error) { // 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) return nil, err } req.Header.Set("User-Agent", "go-external-ip (github.com/glendc/go-external-ip)") // transport to avoid goroutine leak tr := &http.Transport{ MaxIdleConns: 1, IdleConnTimeout: 3 * time.Second, DisableKeepAlives: true, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, DualStack: false, Control: func(network, address string, c syscall.RawConn) error { if protocol == 4 && network == "tcp6" { return errors.New("rejecting ipv6 connection") } else if protocol == 6 && network == "tcp4" { return errors.New("rejecting ipv4 connection") } return nil }, }).DialContext, } client := &http.Client{Timeout: timeout, Transport: tr} // 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) 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) return nil, err } // optionally parse the content raw := string(bytes) 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) return nil, err } } // 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) return nil, InvalidIPError(raw) } // returned the parsed IP return externalIP, nil } go-external-ip-0.1.0/types.go000066400000000000000000000013471413325554400160510ustar00rootroot00000000000000package externalip import ( "log" "net" "time" ) // Source defines the part of a voter which gives the actual voting value (IP). type Source interface { // IP returns IPv4/IPv6 address in a non-error case // net.IP should never be when error is // 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, protocol uint) (net.IP, error) } // voter adds weight to the IP given by a source. // The weight has to be at least 1, and the more it is, the more power the voter has. type voter struct { source Source // provides the IP (see: vote) weight uint // provides the weight of its vote (acts as a multiplier) }