package api
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/url"
"strings"
"time"
)
// connectCause names why an upstream could not be reached. zit says
// "cannot connect" alike for a host that no longer resolves, a server
// that refuses, and a certificate that fails — the three ways a
// long-lived mirror dies look the same on the fleet page (bf5fbed5).
// HTTP statuses zit already names, so only that one message is asked
// about. The probe repeats the first steps of the pull itself: resolve,
// dial, handshake, each bounded, and answers the first that fails, or
// "" when all succeed (the failure was later, inside the sync).
func connectCause(ctx context.Context, rawURL string, pullErr error) string {
if pullErr == nil || !strings.Contains(pullErr.Error(), "cannot connect") {
return ""
}
u, err := url.Parse(rawURL)
if err != nil || u.Hostname() == "" {
return ""
}
host, port := u.Hostname(), u.Port()
if port == "" {
port = "443"
if u.Scheme == "http" {
port = "80"
}
}
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if net.ParseIP(host) == nil {
if _, err := net.DefaultResolver.LookupHost(ctx, host); err != nil {
return "host " + host + " does not resolve — the project may have moved"
}
}
addr := net.JoinHostPort(host, port)
conn, err := (&net.Dialer{}).DialContext(ctx, "tcp", addr)
if err != nil {
var ne net.Error
switch {
case errors.As(err, &ne) && ne.Timeout():
return "no answer from " + addr + " — the server is down or a firewall drops the connection"
case strings.Contains(err.Error(), "refused"):
return addr + " refused the connection — nothing is listening there"
}
return "cannot reach " + addr + ": " + lastClause(err)
}
defer conn.Close()
if u.Scheme != "https" {
return ""
}
tc := tls.Client(conn, &tls.Config{ServerName: host})
if err := tc.HandshakeContext(ctx); err != nil {
return fmt.Sprintf("TLS with %s failed: %s", host, lastClause(err))
}
return ""
}
// lastClause keeps the innermost reason of a wrapped network error:
// "tls: failed to verify certificate: x509: certificate has expired
// or is not yet valid: …" reads as its last part.
func lastClause(err error) string {
s := err.Error()
if i := strings.LastIndex(s, "x509: "); i >= 0 {
return s[i:]
}
if i := strings.LastIndex(s, ": "); i >= 0 {
return s[i+2:]
}
return s
}