Skip to content
quarry Artifact [7089837c6c]
Login

Artifact [7089837c6c]

Artifact 7089837c6c0c353c248e2a73aba84917c9a9e475ff6b896a5e2dba286e2865b8:


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
}