Skip to content

Commit 12fd2e3

Browse files
committed
Probe HTTP Versions to show supported
1 parent 4a582fd commit 12fd2e3

7 files changed

Lines changed: 239 additions & 4 deletions

File tree

frontend/src/components/EndpointCard.vue

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,17 @@
2424
</p>
2525
</div>
2626

27+
<div v-if="httpVersions.length" class="mt-2">
28+
<p class="flex items-center gap-2">
29+
<strong>HTTP Versions:</strong>
30+
<span class="flex flex-wrap gap-2">
31+
<span v-for="p in httpVersions" :key="p.Version" class="badge badge-info">
32+
{{ p.Version }}
33+
</span>
34+
</span>
35+
</p>
36+
</div>
37+
2738
<ul v-if="ssl.PeerCertificates && ssl.PeerCertificates.length" class="list mt-2">
2839
<li class="p-4 pb-2 text-md opacity-80 tracking-wide">Peer Certificates</li>
2940
<li v-for="(cert, idx) in ssl.PeerCertificates" :key="idx" class="list-row">
@@ -48,4 +59,5 @@ const props = defineProps<{
4859
}>();
4960
5061
const tlsProtocols = computed(() => props.ssl?.TLSProtocols?.filter((p) => p.Supported) ?? []);
62+
const httpVersions = computed(() => props.ssl?.HTTPVersions?.filter((p) => p.Supported) ?? []);
5163
</script>

frontend/src/types/certificate.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,16 @@ export interface TLSProtocolSupport {
1313
Supported: boolean;
1414
}
1515

16+
export interface HTTPVersionSupport {
17+
Version: string;
18+
Supported: boolean;
19+
}
20+
1621
export interface SSLDetails {
1722
HandshakeComplete: boolean;
1823
DidResume: boolean;
1924
CipherSuite: number;
2025
PeerCertificates: CertificateDetails[];
2126
TLSProtocols: TLSProtocolSupport[];
27+
HTTPVersions: HTTPVersionSupport[];
2228
}

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,15 @@ require (
77
github.com/google/yamlfmt v0.21.0
88
github.com/likexian/whois v1.15.7
99
github.com/likexian/whois-parser v1.24.21
10+
github.com/quic-go/quic-go v0.59.1
1011
golang.org/x/net v0.52.0
1112
)
1213

1314
require (
15+
github.com/kr/text v0.2.0 // indirect
1416
github.com/likexian/gokit v0.25.16 // indirect
17+
golang.org/x/crypto v0.49.0 // indirect
18+
golang.org/x/sys v0.42.0 // indirect
1519
golang.org/x/text v0.35.0 // indirect
1620
gopkg.in/yaml.v3 v3.0.1 // indirect
1721
)

go.sum

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,40 @@
11
github.com/TwiN/deepmerge v0.2.2 h1:FUG9QMIYg/j2aQyPPhA3XTFJwXSNHI/swaR4Lbyxwg4=
22
github.com/TwiN/deepmerge v0.2.2/go.mod h1:4OHvjV3pPNJCJZBHswYAwk6rxiD8h8YZ+9cPo7nu4oI=
3+
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
4+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
5+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
36
github.com/google/yamlfmt v0.21.0 h1:9FKApQkDpMKgBjwLFytBHUCgqnQgxaQnci0uiESfbzs=
47
github.com/google/yamlfmt v0.21.0/go.mod h1:q6FYExB+Ueu7jZDjKECJk+EaeDXJzJ6Ne0dxx69GWfI=
8+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
9+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
10+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
11+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
512
github.com/likexian/gokit v0.25.16 h1:wwBeUIN/OdoPp6t00xTnZE8Di/+s969Bl5N2Kw6bzP8=
613
github.com/likexian/gokit v0.25.16/go.mod h1:Wqd4f+iifV0qxA1N3MqePJTUsmRy/lpst9/yXriDx/4=
714
github.com/likexian/whois v1.15.7 h1:sajjDhi2bVD71AHJhjV7jLYxN92H4AWhTwxM8hmj7c0=
815
github.com/likexian/whois v1.15.7/go.mod h1:kdPQtYb+7SQVftBEbCblDadUkycN7Mg1k1/Li/rwvmc=
916
github.com/likexian/whois-parser v1.24.21 h1:MxsrGRxDOiZIVp7q7N/yAIbKuN4QAkGjCpOtTDA5OsM=
1017
github.com/likexian/whois-parser v1.24.21/go.mod h1:o3DUruO65Pb8WXCJCTlSVkTbwuYVrBCeoMTw2q0mxY4=
18+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
19+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
20+
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
21+
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
22+
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
23+
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
24+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
25+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
26+
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
27+
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
28+
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
29+
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
1130
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
1231
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
32+
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
33+
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
1334
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
1435
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
15-
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
1636
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
37+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
38+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
1739
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
1840
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/certificate/certificate.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type SSLDetails struct {
1414
CipherSuite uint16
1515
PeerCertificates []CertificateDetails
1616
TLSProtocols []TLSProtocolSupport
17+
HTTPVersions []HTTPVersionSupport
1718
}
1819

1920
type CertificateDetails struct {
@@ -42,14 +43,19 @@ func GetCertificateInfo(client *http.Client, address domain.Domain) (*SSLDetails
4243
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
4344
}
4445

45-
// Probe supported TLS versions in parallel with the main HTTPS request
4646
var probeWG sync.WaitGroup
4747
var probes []TLSProtocolSupport
48+
var httpProbes []HTTPVersionSupport
4849
if hostPort, err := addressToHostPort(string(address)); err == nil {
49-
probeWG.Add(1)
50+
timeout := probeTimeout(client)
51+
probeWG.Add(2)
5052
go func() {
5153
defer probeWG.Done()
52-
probes = probeTLSVersions(hostPort, probeTimeout(client))
54+
probes = probeTLSVersions(hostPort, timeout)
55+
}()
56+
go func() {
57+
defer probeWG.Done()
58+
httpProbes = probeHTTPVersions(hostPort, timeout)
5359
}()
5460
}
5561

@@ -78,6 +84,7 @@ func GetCertificateInfo(client *http.Client, address domain.Domain) (*SSLDetails
7884

7985
probeWG.Wait()
8086
sslInfo.TLSProtocols = probes
87+
sslInfo.HTTPVersions = httpProbes
8188
return &sslInfo, nil
8289
}
8390

internal/certificate/http_probe.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package certificate
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"net"
7+
"sync"
8+
"time"
9+
10+
"github.com/quic-go/quic-go"
11+
)
12+
13+
type HTTPVersionSupport struct {
14+
Version string
15+
Supported bool
16+
}
17+
18+
var probedHTTPVersions = []struct {
19+
alpn string
20+
name string
21+
quic bool
22+
}{
23+
{"http/1.1", "HTTP/1.1", false},
24+
{"h2", "HTTP/2", false},
25+
{"h3", "HTTP/3", true},
26+
}
27+
28+
// Shared so quic-go's UDP buffer-size check logs at most once at process startup
29+
// (per quic-go/wiki/UDP-Buffer-Sizes), and so probes reuse a single UDP socket.
30+
var (
31+
h3TransportOnce sync.Once
32+
h3Transport *quic.Transport
33+
)
34+
35+
func sharedH3Transport() *quic.Transport {
36+
h3TransportOnce.Do(func() {
37+
udp, err := net.ListenUDP("udp", &net.UDPAddr{})
38+
if err != nil {
39+
return
40+
}
41+
h3Transport = &quic.Transport{Conn: udp}
42+
})
43+
return h3Transport
44+
}
45+
46+
func probeHTTPVersions(hostPort string, timeout time.Duration) []HTTPVersionSupport {
47+
results := make([]HTTPVersionSupport, len(probedHTTPVersions))
48+
var wg sync.WaitGroup
49+
dialer := &net.Dialer{Timeout: timeout}
50+
for i, pv := range probedHTTPVersions {
51+
wg.Add(1)
52+
go func(i int, alpn, name string, isQUIC bool) {
53+
defer wg.Done()
54+
results[i].Version = name
55+
if isQUIC {
56+
results[i].Supported = probeH3(hostPort, alpn, timeout)
57+
return
58+
}
59+
results[i].Supported = probeALPN(dialer, hostPort, alpn)
60+
}(i, pv.alpn, pv.name, pv.quic)
61+
}
62+
wg.Wait()
63+
return results
64+
}
65+
66+
func probeALPN(dialer *net.Dialer, hostPort, alpn string) bool {
67+
conn, err := tls.DialWithDialer(dialer, "tcp", hostPort, &tls.Config{
68+
InsecureSkipVerify: true,
69+
NextProtos: []string{alpn},
70+
})
71+
if err != nil {
72+
return false
73+
}
74+
defer conn.Close()
75+
return conn.ConnectionState().NegotiatedProtocol == alpn
76+
}
77+
78+
func probeH3(hostPort, alpn string, timeout time.Duration) bool {
79+
tr := sharedH3Transport()
80+
if tr == nil {
81+
return false
82+
}
83+
addr, err := net.ResolveUDPAddr("udp", hostPort)
84+
if err != nil {
85+
return false
86+
}
87+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
88+
defer cancel()
89+
conn, err := tr.Dial(ctx, addr, &tls.Config{
90+
InsecureSkipVerify: true,
91+
NextProtos: []string{alpn},
92+
}, nil)
93+
if err != nil {
94+
return false
95+
}
96+
_ = conn.CloseWithError(0, "")
97+
return true
98+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package certificate
2+
3+
import (
4+
"crypto/tls"
5+
"net"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
"time"
10+
)
11+
12+
func startALPNServer(t *testing.T, protos []string) (string, func()) {
13+
t.Helper()
14+
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
15+
srv.TLS = &tls.Config{NextProtos: protos}
16+
srv.StartTLS()
17+
hostPort, err := addressToHostPort(srv.URL)
18+
if err != nil {
19+
srv.Close()
20+
t.Fatalf("addressToHostPort(%q): %v", srv.URL, err)
21+
}
22+
return hostPort, srv.Close
23+
}
24+
25+
func TestProbeHTTPVersions_PinnedServer(t *testing.T) {
26+
hostPort, cleanup := startALPNServer(t, []string{"h2", "http/1.1"})
27+
defer cleanup()
28+
29+
results := probeHTTPVersions(hostPort, 3*time.Second)
30+
31+
if len(results) != len(probedHTTPVersions) {
32+
t.Fatalf("expected %d results, got %d", len(probedHTTPVersions), len(results))
33+
}
34+
35+
for _, r := range results {
36+
switch r.Version {
37+
case "HTTP/1.1", "HTTP/2":
38+
if !r.Supported {
39+
t.Errorf("expected %s to be supported", r.Version)
40+
}
41+
case "HTTP/3":
42+
if r.Supported {
43+
t.Errorf("expected HTTP/3 NOT supported against a TLS-only test server")
44+
}
45+
default:
46+
t.Errorf("unexpected version %q", r.Version)
47+
}
48+
}
49+
}
50+
51+
func TestProbeHTTPVersions_HTTP11Only(t *testing.T) {
52+
hostPort, cleanup := startALPNServer(t, []string{"http/1.1"})
53+
defer cleanup()
54+
55+
results := probeHTTPVersions(hostPort, 3*time.Second)
56+
57+
for _, r := range results {
58+
switch r.Version {
59+
case "HTTP/1.1":
60+
if !r.Supported {
61+
t.Errorf("expected HTTP/1.1 supported")
62+
}
63+
default:
64+
if r.Supported {
65+
t.Errorf("expected %s NOT supported by HTTP/1.1-only server", r.Version)
66+
}
67+
}
68+
}
69+
}
70+
71+
func TestProbeHTTPVersions_UnreachableHost(t *testing.T) {
72+
lis, err := net.Listen("tcp", "127.0.0.1:0")
73+
if err != nil {
74+
t.Fatalf("listen: %v", err)
75+
}
76+
addr := lis.Addr().String()
77+
lis.Close()
78+
79+
results := probeHTTPVersions(addr, 2*time.Second)
80+
81+
for _, r := range results {
82+
if r.Supported {
83+
t.Errorf("%s should not be supported against closed port", r.Version)
84+
}
85+
}
86+
}

0 commit comments

Comments
 (0)