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
127 lines
3.2 KiB
Go
127 lines
3.2 KiB
Go
package externalip
|
|
|
|
import (
|
|
"context"
|
|
"io/ioutil"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"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, ipversion uint) *HTTPSource {
|
|
return &HTTPSource{
|
|
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
|
|
ipversion uint
|
|
}
|
|
|
|
// 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) (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 {
|
|
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{
|
|
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 {
|
|
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 {
|
|
if logger != 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 {
|
|
if logger != 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 {
|
|
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
|
|
}
|