Skip to content

Commit 89e2295

Browse files
Add diagnostics callback for tlscommon configs and httptransport (#207)
Add diagnostics callbacks for TLS configuration to make it easier to include in diagnostics bundles. Diagnostics info will geneally verify that the certs or cas are able to load, and display some basic info such as cert names, expiry times, fingerprints, etc. httptransport callback will attempt an HTTP request and return information collected from httptrace about the request. Errors from http connections will be parsed to try to output a human-readable cause. --------- Co-authored-by: Tiago Queiroz <[email protected]>
1 parent d9dd081 commit 89e2295

File tree

5 files changed

+908
-0
lines changed

5 files changed

+908
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
build/
22
.idea
33
.vscode
4+
*.swp

transport/httpcommon/diag.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
// Licensed to Elasticsearch B.V. under one or more contributor
2+
// license agreements. See the NOTICE file distributed with
3+
// this work for additional information regarding copyright
4+
// ownership. Elasticsearch B.V. licenses this file to you under
5+
// the Apache License, Version 2.0 (the "License"); you may
6+
// not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package httpcommon
19+
20+
import (
21+
"bytes"
22+
"crypto/tls"
23+
"crypto/x509"
24+
"errors"
25+
"fmt"
26+
"io"
27+
"log"
28+
"net"
29+
"net/http"
30+
"net/http/httptrace"
31+
"net/textproto"
32+
"net/url"
33+
34+
"github.com/elastic/elastic-agent-libs/transport/tlscommon"
35+
)
36+
37+
// DiagRequest returns a diagnostics hook callback that will send the passed requests using a roundtripper generated from the settings and log httptrace events in the returned bytes.
38+
func (settings *HTTPTransportSettings) DiagRequests(reqs []*http.Request, opts ...TransportOption) func() []byte {
39+
if settings == nil {
40+
return func() []byte {
41+
return []byte(`error: nil httpcommon.HTTPTransportSettings`)
42+
}
43+
}
44+
if len(reqs) == 0 {
45+
return func() []byte {
46+
return []byte(`error: 0 requests`)
47+
}
48+
}
49+
return func() []byte {
50+
var b bytes.Buffer
51+
rt, err := settings.RoundTripper(opts...)
52+
if err != nil {
53+
b.WriteString("unable to get roundtripper: " + err.Error())
54+
return b.Bytes()
55+
}
56+
logger := log.New(&b, "", log.LstdFlags|log.Lmicroseconds|log.LUTC)
57+
if settings.TLS == nil {
58+
logger.Print("No TLS settings")
59+
} else {
60+
logger.Print("TLS settings detected")
61+
}
62+
logger.Printf("Proxy disable=%v url=%s", settings.Proxy.Disable, settings.Proxy.URL)
63+
64+
ct := &httptrace.ClientTrace{
65+
GetConn: func(hostPort string) {
66+
logger.Printf("GetConn called for %q", hostPort)
67+
},
68+
GotConn: func(connInfo httptrace.GotConnInfo) {
69+
logger.Printf("GotConn for %q", connInfo.Conn.RemoteAddr())
70+
},
71+
GotFirstResponseByte: func() {
72+
logger.Print("Response started")
73+
},
74+
Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
75+
logger.Printf("Got info response status=%d, headers=%v", code, header)
76+
return nil
77+
},
78+
DNSStart: func(info httptrace.DNSStartInfo) {
79+
logger.Printf("Starting DNS lookup for %q", info.Host)
80+
},
81+
DNSDone: func(info httptrace.DNSDoneInfo) {
82+
logger.Printf("Done DNS lookup: %+v", info)
83+
},
84+
ConnectStart: func(network, addr string) {
85+
logger.Printf("Connection started to %s:%s", network, addr)
86+
},
87+
ConnectDone: func(network, addr string, err error) {
88+
logger.Printf("Connection to %s:%s done, err: %v", network, addr, err)
89+
},
90+
TLSHandshakeStart: func() {
91+
logger.Print("TLS handshake starting")
92+
},
93+
TLSHandshakeDone: func(state tls.ConnectionState, err error) {
94+
logger.Printf("TLS handshake done. state=%+v err=%v", state, err)
95+
logger.Printf("Peer certificate count %d", len(state.PeerCertificates))
96+
for i, crt := range state.PeerCertificates {
97+
logger.Printf("- Peer Certificate %d\n\t%s", i, tlscommon.CertDiagString(crt))
98+
}
99+
100+
logger.Printf("Verified chains count: %d", len(state.VerifiedChains))
101+
for i, chain := range state.VerifiedChains {
102+
for j, crt := range chain {
103+
logger.Printf("- Chain %d certificate %d\n\t%s", i, j, tlscommon.CertDiagString(crt))
104+
}
105+
}
106+
},
107+
WroteHeaders: func() {
108+
logger.Printf("Wrote request headers")
109+
},
110+
Wait100Continue: func() {
111+
logger.Printf("Waiting for continue")
112+
},
113+
WroteRequest: func(info httptrace.WroteRequestInfo) {
114+
logger.Printf("Wrote request err=%v", info.Err)
115+
},
116+
}
117+
for i, req := range reqs {
118+
logger.Printf("Request %d to %s starting", i, req.URL.String())
119+
req = req.WithContext(httptrace.WithClientTrace(req.Context(), ct))
120+
if resp, err := rt.RoundTrip(req); err != nil {
121+
logger.Printf("request %d error: %s", i, diagError(err))
122+
} else if isGoHTTPResp(resp) {
123+
resp.Body.Close()
124+
logger.Printf("request %d error: HTTP request sent to HTTPS server.", i)
125+
} else {
126+
resp.Body.Close()
127+
logger.Printf("request %d successful. status=%d", i, resp.StatusCode)
128+
}
129+
}
130+
return b.Bytes()
131+
}
132+
}
133+
134+
// isGoHTTPResp detects if the response is one that a go http.Server sends if an HTTP request is made to an HTTPS server.
135+
// non Go servers may return a net.OpError instead.
136+
func isGoHTTPResp(r *http.Response) bool {
137+
if r.StatusCode != http.StatusBadRequest {
138+
return false
139+
}
140+
p, err := io.ReadAll(r.Body)
141+
if err != nil {
142+
return false
143+
}
144+
return string(p) == "Client sent an HTTP request to an HTTPS server.\n"
145+
}
146+
147+
// diagError tries to diagnose the error and return a cause/possible cause in a human readable format.
148+
// If no matching errors are found err.Error is returned.
149+
func diagError(err error) string {
150+
// client does not support server algorithm
151+
if errors.Is(err, x509.ErrUnsupportedAlgorithm) {
152+
return fmt.Sprintf("%v: caused by client does not support server's signature algorithm.", err)
153+
}
154+
155+
// a *net.OpError could indicate an HTTP request made to an HTTPS server
156+
var netErr *net.OpError
157+
if errors.As(err, &netErr) {
158+
if netErr.Err.Error() == "read: connection reset by peer" {
159+
return fmt.Sprintf("%v: possible cause: HTTP schema used for HTTPS server.", netErr)
160+
}
161+
}
162+
163+
// Client does not have CA that matches server cert
164+
var unknownAuthErr x509.UnknownAuthorityError
165+
if errors.As(err, &unknownAuthErr) {
166+
return fmt.Sprintf("%v: caused by no trusted client CA.", err)
167+
}
168+
169+
// CA is ok but the server's cert is not.
170+
var certValidErr x509.CertificateInvalidError
171+
if errors.As(err, &certValidErr) {
172+
return fmt.Sprintf("%v: caused by invalid server certificate.", certValidErr)
173+
}
174+
175+
// cert validation error can indicate that a custom CA needs to be used
176+
var tlsErr *tls.CertificateVerificationError
177+
if errors.As(err, &tlsErr) {
178+
return fmt.Sprintf("%v: possible cause: client TLS settings incorrect.", tlsErr)
179+
}
180+
181+
// keep unwrapping to url.Error as the last error as other failures can be embedded in a url.Error
182+
// Can detect if an HTTPS request is made to an HTTP server
183+
var uErr *url.Error
184+
if errors.As(err, &uErr) {
185+
switch uErr.Err.Error() {
186+
case "http: server gave HTTP response to HTTPS client":
187+
return fmt.Sprintf("%v: caused by using HTTPS schema on HTTP server.", uErr)
188+
case "remote error: tls: certificate required":
189+
return fmt.Sprintf("%v: caused by missing mTLS client cert.", uErr)
190+
case "remote error: tls: expired certificate":
191+
return fmt.Sprintf("%v: caused by expired mTLS client cert.", uErr)
192+
case "remote error: tls: bad certificate":
193+
return fmt.Sprintf("%v: caused by invalid mTLS client cert, does the server trust the CA used for the client cert?.", uErr)
194+
}
195+
}
196+
197+
return err.Error()
198+
}

0 commit comments

Comments
 (0)