Skip to content

Instantly share code, notes, and snippets.

@rikonor
Created February 9, 2017 21:56
Show Gist options
  • Save rikonor/f29d0236ce512d96282850d74b5f02e2 to your computer and use it in GitHub Desktop.
Save rikonor/f29d0236ce512d96282850d74b5f02e2 to your computer and use it in GitHub Desktop.
package publicip
import (
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"strings"
"sync"
"time"
"golang.org/x/sync/errgroup"
)
// source defines a source for an IP and how to handle it
// sources are assumed to be reliable
type source struct {
sourceURL string
responseHandler func(r io.Reader) (net.IP, error)
}
var sources = []source{
source{
sourceURL: "http://myexternalip.com/raw",
responseHandler: textResponseHandler,
},
source{
sourceURL: "http://ifconfig.co",
responseHandler: textResponseHandler,
},
source{
sourceURL: "https://api.ipify.org",
responseHandler: textResponseHandler,
},
source{
sourceURL: "http://ip-api.com/json",
responseHandler: ipapiResponseHandler,
},
}
// textResponseHandler handle simple responses that contain just the IP
// e.g. `127.0.0.1\n`
func textResponseHandler(r io.Reader) (net.IP, error) {
// parse response for IP
ipbytes, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
// sanitize response
ipstr := string(ipbytes)
ipstr = strings.Trim(ipstr, " \n")
// parse response
ip := net.ParseIP(ipstr)
if ip == nil {
return nil, fmt.Errorf("invalid ip address: %s", ipstr)
}
return ip, nil
}
// ipapiResponseHandler http://ip-api.com/json
func ipapiResponseHandler(r io.Reader) (net.IP, error) {
type Response struct {
Query string `json:"query"`
}
// parse response
var res Response
if err := json.NewDecoder(r).Decode(&res); err != nil {
return nil, err
}
ip := net.ParseIP(res.Query)
if ip == nil {
return nil, fmt.Errorf("invalid ip address: %s", res.Query)
}
return ip, nil
}
// PublicIP returns the public IP of the calling host
// it will attempt multiple sources in case one fails
// note: if one of the sources failed - we still return its error along with the host IP
func PublicIP() (net.IP, error) {
client := &http.Client{Timeout: 10 * time.Second}
ctx, cancel := context.WithCancel(context.Background())
// make sure all requests exit at the end
defer cancel()
var ip net.IP
ipOnce := &sync.Once{}
setIP := func(x net.IP) {
ipOnce.Do(func() {
ip = x
})
}
eg := &errgroup.Group{}
for _, s := range sources {
func(s source) {
eg.Go(func() error {
return s.handle(client, ctx, cancel, setIP)
})
}(s)
}
err := eg.Wait()
return ip, err
}
func (s source) handle(client *http.Client, ctx context.Context, cancel context.CancelFunc, setIP func(net.IP)) error {
// prep request
req, err := http.NewRequest("GET", s.sourceURL, nil)
if err != nil {
return err
}
// wrap request in context
req = req.WithContext(ctx)
// make the request
res, err := client.Do(req)
// check if the request was cancelled
select {
case <-ctx.Done():
if ctx.Err() == context.Canceled {
// aborting due to another request finishing first
return nil
}
default:
}
// check if request failed
if err != nil {
return err
}
defer res.Body.Close()
// check if request failed
if res.StatusCode != http.StatusOK {
return fmt.Errorf("request failed: %d", res.StatusCode)
}
// handle the response and extract the IP
resIP, err := s.responseHandler(res.Body)
if err != nil {
return err
}
// set the IP
setIP(resIP)
// cancel the other requests
cancel()
return nil
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment