|
| 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