package app import ( "bytes" "context" "crypto/tls" "crypto/x509" "encoding/binary" "fmt" "io" "io/ioutil" "net/http" "os" "os/signal" "sync" "syscall" "time" "github.com/ledgerwatch/log/v3" "github.com/urfave/cli/v2" "golang.org/x/net/http2" ) var ( diagnosticsURLFlag = cli.StringFlag{ Name: "diagnostics.url", Usage: "URL of the diagnostics system provided by the support team, include unique session PIN", } metricsURLsFlag = cli.StringSliceFlag{ Name: "metrics.urls", Usage: "Comma separated list of URLs to the metrics endpoints thats are being diagnosed", } ) var supportCommand = cli.Command{ Action: MigrateFlags(connectDiagnostics), Name: "support", Usage: "Connect Erigon instance to a diagnostics system for support", ArgsUsage: "--diagnostics.url --metrics.url ", Flags: []cli.Flag{ &metricsURLsFlag, &diagnosticsURLFlag, }, Category: "SUPPORT COMMANDS", Description: ` The support command connects a running Erigon instances to a diagnostics system specified by the URL.`, } // Conn is client/server symmetric connection. // It implements the io.Reader/io.Writer/io.Closer to read/write or close the connection to the other side. // It also has a Send/Recv function to use channels to communicate with the other side. type Conn struct { r io.Reader wc io.WriteCloser cancel context.CancelFunc wLock sync.Mutex rLock sync.Mutex } func connectDiagnostics(cliCtx *cli.Context) error { sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) ctx, cancel := context.WithCancel(context.Background()) go func() { <-sigs cancel() }() pollInterval := 500 * time.Millisecond pollEvery := time.NewTicker(pollInterval) defer pollEvery.Stop() client := &http.Client{} defer client.CloseIdleConnections() metricsURLs := cliCtx.StringSlice(metricsURLsFlag.Name) metricsURL := metricsURLs[0] // TODO: Generalise diagnosticsUrl := cliCtx.String(diagnosticsURLFlag.Name) // Create a pool with the server certificate since it is not signed // by a known CA certPool := x509.NewCertPool() srvCert, err := ioutil.ReadFile("diagnostics.crt") if err != nil { return fmt.Errorf("reading server certificate: %v", err) } caCert, err := ioutil.ReadFile("CA-cert.pem") if err != nil { return fmt.Errorf("reading server certificate: %v", err) } certPool.AppendCertsFromPEM(srvCert) certPool.AppendCertsFromPEM(caCert) // Create TLS configuration with the certificate of the server tlsConfig := &tls.Config{ RootCAs: certPool, InsecureSkipVerify: true, //nolint:gosec } reader, writer := io.Pipe() httpClient := &http.Client{Transport: &http2.Transport{TLSClientConfig: tlsConfig}} // Create a request object to send to the server req, err := http.NewRequest(http.MethodPost, diagnosticsUrl, reader) if err != nil { return err } // Apply custom headers // Apply given context to the sent request req = req.WithContext(ctx) // Perform the request resp, err := httpClient.Do(req) if err != nil { return err } // Create a connection ctx1, cancel1 := context.WithCancel(req.Context()) defer cancel1() defer resp.Body.Close() defer writer.Close() // Apply the connection context on the request context resp.Request = req.WithContext(ctx1) var metricsBuf bytes.Buffer outerLoop: for { var buf [4096]byte var readLen int for readLen < len(buf) && (readLen == 0 || buf[readLen-1] != '\n') { len, err := resp.Body.Read(buf[readLen:]) if err != nil { log.Error("Connection read", "err", err) break outerLoop } readLen += len } if buf[readLen-1] != '\n' { log.Error("Request too long, circuit breaker") break outerLoop } fmt.Printf("Got request: %s\n", buf[:readLen-1]) metricsResponse, err := client.Get(metricsURL + string(buf[:readLen-1])) if err != nil { log.Error("Problem requesting metrics", "url", metricsURL, "query", string(buf[:readLen-1]), "err", err) break outerLoop } // Buffer the metrics response, and relay it back to the diagnostics system, prepending with the size metricsBuf.Reset() if _, err := io.Copy(&metricsBuf, metricsResponse.Body); err != nil { metricsResponse.Body.Close() log.Error("Problem extracting metrics", "url", metricsURL, "query", string(buf[:readLen-1]), "err", err) break outerLoop } metricsResponse.Body.Close() fmt.Printf("Got response:\n%s\n", metricsBuf.Bytes()) var sizeBuf [4]byte binary.BigEndian.PutUint32(sizeBuf[:], uint32(metricsBuf.Len())) if _, err := writer.Write(sizeBuf[:]); err != nil { log.Error("Problem relaying metrics prefix len", "url", metricsURL, "query", string(buf[:readLen-1]), "err", err) break outerLoop } if _, err := writer.Write(metricsBuf.Bytes()); err != nil { log.Error("Problem relaying", "url", metricsURL, "query", string(buf[:readLen-1]), "err", err) break outerLoop } } return nil }