Created
February 9, 2017 21:56
-
-
Save rikonor/f29d0236ce512d96282850d74b5f02e2 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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