diff --git a/pkg/api/api.go b/pkg/api/api.go index 8f1dee24..7dbd5628 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -2,8 +2,10 @@ package api import ( "errors" + "fmt" "net" "net/http" + "strings" "github.com/wakatime/wakatime-cli/pkg/log" ) @@ -11,6 +13,10 @@ import ( const ( // BaseURL is the base url of the wakatime api. BaseURL = "https://api.wakatime.com/api/v1" + // baseIPAddrv4 is the base ip address v4 of the wakatime api. + baseIPAddrv4 = "143.244.210.202" + // baseIPAddrv6 is the base ip address v6 of the wakatime api. + baseIPAddrv6 = "2604:a880:4:1d0::2a7:b000" // DefaultTimeoutSecs is the default timeout used for requests to the wakatime api. DefaultTimeoutSecs = 120 ) @@ -59,19 +65,51 @@ func NewClient(baseURL string, opts ...Option) *Client { func (c *Client) Do(req *http.Request) (*http.Response, error) { resp, err := c.doFunc(c, req) if err != nil { + // don't set alternate host if there's a custom api url + if !strings.HasPrefix(c.baseURL, BaseURL) { + return nil, err + } + var dnsError *net.DNSError - if errors.As(err, &dnsError) { - log.Warnf("dns error: %s. Retrying with fallback dns resolver", req.URL) + if !errors.As(err, &dnsError) { + return nil, err + } + + t, err := NewTransportWithHostVerificationDisabled() + if err != nil { + return nil, err + } + + c.client = &http.Client{ + Transport: t, + } - c.client = &http.Client{ - Transport: NewTransportWithCloudfareDNS(), - } + var alternateHost = baseIPAddrv4 - return c.doFunc(c, req) + if isLocalIPv6() { + alternateHost = baseIPAddrv6 } - return nil, err + req.URL.Host = alternateHost + + log.Warnf("dns error, it will retry with host ip address '%s'", alternateHost) + + return c.doFunc(c, req) } return resp, nil } + +func isLocalIPv6() bool { + conn, err := net.Dial("udp", fmt.Sprintf("%s:80", baseIPAddrv4)) + if err != nil { + log.Warnf("failed dialing to detect default local ip address: %s", err) + return true + } + + defer conn.Close() + + localAddr := conn.LocalAddr().(*net.UDPAddr) + + return localAddr.IP.To4() == nil +} diff --git a/pkg/api/transport.go b/pkg/api/transport.go index bebe5d86..f85b1afd 100644 --- a/pkg/api/transport.go +++ b/pkg/api/transport.go @@ -1,16 +1,16 @@ package api import ( - "context" + "crypto/tls" "crypto/x509" - "net" "net/http" - "strings" "time" "github.com/wakatime/wakatime-cli/pkg/log" ) +const serverName = "api.wakatime.com" + // NewTransport initializes a new http.Transport. func NewTransport() *http.Transport { return &http.Transport{ @@ -23,31 +23,17 @@ func NewTransport() *http.Transport { } } -// NewTransportWithCloudfareDNS initializes a new http.Transport with cloudfare DNS resolver. -func NewTransportWithCloudfareDNS() *http.Transport { +// NewTransportWithHostVerificationDisabled initializes a new http.Transport with disabled host verification. +func NewTransportWithHostVerificationDisabled() (*http.Transport, error) { t := NewTransport() - t.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) { - r := &net.Resolver{ - PreferGo: false, - Dial: func(ctx context.Context, network, address string) (net.Conn, error) { - d := net.Dialer{ - Timeout: DefaultTimeoutSecs * time.Second, - } - - // ipv6 only - if strings.HasSuffix(network, "6") { - return d.DialContext(ctx, network, "[2606:4700:4700::1111]:53") - } - - return d.DialContext(ctx, network, "1.1.1.1:53") - }, - } - - return r.Dial(ctx, network, addr) + t.TLSClientConfig = &tls.Config{ + MinVersion: tls.VersionTLS12, + RootCAs: CACerts(), + ServerName: serverName, } - return t + return t, nil } // LazyCreateNewTransport uses the client's Transport if exists, or creates a new one.