diff --git a/plugin/flags.go b/plugin/flags.go index b571cad..29d28b1 100644 --- a/plugin/flags.go +++ b/plugin/flags.go @@ -41,7 +41,7 @@ func Flags() []cli.Flag { // Plugin flags flags = append(flags, loggingFlags(FlagsPluginCategory)...) - flags = append(flags, httpClientFlags(FlagsPluginCategory)...) + flags = append(flags, networkFlags(FlagsPluginCategory)...) return flags } diff --git a/plugin/http.go b/plugin/network.go similarity index 72% rename from plugin/http.go rename to plugin/network.go index ae4ebd7..76cefd3 100644 --- a/plugin/http.go +++ b/plugin/network.go @@ -22,7 +22,9 @@ import ( "net/http" "time" + "github.com/rs/zerolog" "github.com/rs/zerolog/log" + "github.com/thegeeklab/wp-plugin-go/trace" "github.com/urfave/cli/v2" "golang.org/x/net/proxy" ) @@ -34,7 +36,23 @@ const ( HTTPTransportMaxIdleConns = 100 ) -func httpClientFlags(category string) []cli.Flag { +// Network contains options for connecting to the network. +type Network struct { + // Context for making network requests. + // + // If `trace` logging is requested the context will use `httptrace` to + // capture all network requests. + //nolint:containedctx + Context context.Context + + /// Whether SSL verification is skipped + SkipVerify bool + + // Client for making network requests. + Client *http.Client +} + +func networkFlags(category string) []cli.Flag { return []cli.Flag{ &cli.BoolFlag{ Name: "transport.skip-verify", @@ -57,11 +75,12 @@ func httpClientFlags(category string) []cli.Flag { } } -func HTTPClientFromContext(ctx *cli.Context) *http.Client { +func NetworkFromContext(ctx *cli.Context) Network { var ( - skip = ctx.Bool("transport.skip-verify") - socks = ctx.String("transport.socks-proxy") - socksoff = ctx.Bool("transport.socks-proxy-off") + skip = ctx.Bool("transport.skip-verify") + defaultContext = context.Background() + socks = ctx.String("transport.socks-proxy") + socksoff = ctx.Bool("transport.socks-proxy-off") ) certs, err := x509.SystemCertPool() @@ -106,7 +125,17 @@ func HTTPClientFromContext(ctx *cli.Context) *http.Client { transport.DialContext = dialer.DialContext } - return &http.Client{ + if zerolog.GlobalLevel() == zerolog.TraceLevel { + defaultContext = trace.HTTP(defaultContext) + } + + client := &http.Client{ Transport: transport, } + + return Network{ + Context: defaultContext, + SkipVerify: skip, + Client: client, + } } diff --git a/plugin/plugin.go b/plugin/plugin.go index 1dfdb8a..27c56e1 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -17,7 +17,6 @@ package plugin import ( "context" "fmt" - "net/http" "os" "strings" @@ -46,7 +45,8 @@ type Options struct { type Plugin struct { app *cli.App execute ExecuteFunc - client *http.Client + // Network options. + Network Network // Metadata of the current pipeline. Metadata Metadata } @@ -87,7 +87,7 @@ func (p *Plugin) action(ctx *cli.Context) error { } p.Metadata = MetadataFromContext(ctx) - p.client = HTTPClientFromContext(ctx) + p.Network = NetworkFromContext(ctx) if p.execute == nil { panic("plugin execute function is not set") @@ -96,11 +96,6 @@ func (p *Plugin) action(ctx *cli.Context) error { return p.execute(ctx.Context, ctx) } -// HTTPClient returns the http.Client instance. -func (p *Plugin) HTTPClient() *http.Client { - return p.client -} - // Run the plugin. func (p *Plugin) Run() { if err := p.app.Run(os.Args); err != nil { diff --git a/trace/http.go b/trace/http.go new file mode 100644 index 0000000..2f2a065 --- /dev/null +++ b/trace/http.go @@ -0,0 +1,119 @@ +// Copyright (c) 2019, Drone Plugins project authors +// Copyright (c) 2021, Robert Kaussow + +// Use of this source code is governed by an Apache 2.0 license that can be +// found in the LICENSE file. + +package trace + +import ( + "context" + "crypto/tls" + "fmt" + "net/http/httptrace" + "net/textproto" + + "github.com/rs/zerolog/log" +) + +// HTTP uses httptrace to log all network activity for HTTP requests. +func HTTP(ctx context.Context) context.Context { + return httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{ + GetConn: func(hostPort string) { + log.Trace().Str("host-port", hostPort).Msg("ClientTrace.GetConn") + }, + + GotConn: func(connInfo httptrace.GotConnInfo) { + log.Trace(). + Str("local-address", connInfo.Conn.LocalAddr().String()). + Str("remote-address", connInfo.Conn.RemoteAddr().String()). + Bool("reused", connInfo.Reused). + Bool("was-idle", connInfo.WasIdle). + Dur("idle-time", connInfo.IdleTime). + Msg("ClientTrace.GoConn") + }, + + PutIdleConn: func(err error) { + log.Trace().Err(err).Msg("ClientTrace.GoConn") + }, + + GotFirstResponseByte: func() { + log.Trace().Msg("ClientTrace.GotFirstResponseByte") + }, + + Got100Continue: func() { + log.Trace().Msg("ClientTrace.Got100Continue") + }, + + Got1xxResponse: func(code int, header textproto.MIMEHeader) error { + log.Trace(). + Int("code", code). + Str("header", fmt.Sprint(header)). + Msg("ClientTrace.Got1xxxResponse") + + return nil + }, + + DNSStart: func(dnsInfo httptrace.DNSStartInfo) { + log.Trace().Str("host", dnsInfo.Host).Msg("ClientTrace.DNSStart") + }, + + DNSDone: func(dnsInfo httptrace.DNSDoneInfo) { + log.Trace(). + Str("addresses", fmt.Sprint(dnsInfo.Addrs)). + Err(dnsInfo.Err). + Bool("coalesced", dnsInfo.Coalesced). + Msg("ClientTrace.DNSDone") + }, + + ConnectStart: func(network, addr string) { + log.Trace(). + Str("network", network). + Str("address", addr). + Msg("ClientTrace.ConnectStart") + }, + + ConnectDone: func(network, addr string, err error) { + log.Trace(). + Str("network", network). + Str("address", addr). + Err(err). + Msg("ClientTrace.ConnectDone") + }, + + TLSHandshakeStart: func() { + log.Trace().Msg("ClientTrace.TLSHandshakeStart") + }, + + TLSHandshakeDone: func(connState tls.ConnectionState, err error) { + log.Trace(). + Uint16("version", connState.Version). + Bool("handshake-complete", connState.HandshakeComplete). + Bool("did-resume", connState.DidResume). + Uint16("cipher-suite", connState.CipherSuite). + Str("negotiated-protocol", connState.NegotiatedProtocol). + Str("server-name", connState.ServerName). + Err(err). + Msg("ClientTrace.TLSHandshakeDone") + }, + + WroteHeaderField: func(key string, value []string) { + log.Trace(). + Str("key", key). + Strs("values", value). + Msg("ClientTrace.WroteHeaderField") + }, + + WroteHeaders: func() { + log.Trace().Msg("ClientTrace.WroteHeaders") + }, + + Wait100Continue: func() { + log.Trace().Msg("ClientTrace.Wait100Continue") + }, + + WroteRequest: func(reqInfo httptrace.WroteRequestInfo) { + log.Trace().Err(reqInfo.Err).Msg("ClientTrace.WroteRequest") + }, + }) +}