github.com/baptiste-b-pegasys/quorum/v22@v22.4.2/plugin/central.go (about)

     1  package plugin
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/tls"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"net"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"runtime"
    14  	"strings"
    15  	"text/template"
    16  	"time"
    17  
    18  	"github.com/ethereum/go-ethereum/log"
    19  )
    20  
    21  // Central https centralClient communicating with Plugin Central
    22  type CentralClient struct {
    23  	config     *PluginCentralConfiguration
    24  	httpClient *http.Client
    25  }
    26  
    27  // Create New Central Client
    28  func NewPluginCentralClient(config *PluginCentralConfiguration) *CentralClient {
    29  	c := &CentralClient{
    30  		config: config,
    31  	}
    32  	c.httpClient = &http.Client{}
    33  	c.httpClient.Transport = &http.Transport{
    34  		DialTLS: c.getNewSecureDialer(),
    35  	}
    36  	return c
    37  }
    38  
    39  // Builds a Dialer that supports CA Verification & Certificate Pinning.
    40  func (cc *CentralClient) getNewSecureDialer() Dialer {
    41  	return func(network, addr string) (net.Conn, error) {
    42  		c, err := tls.Dial(network, addr, &tls.Config{InsecureSkipVerify: cc.config.InsecureSkipTLSVerify})
    43  		if err != nil {
    44  			return c, err
    45  		}
    46  		// support certificate pinning?
    47  		if cc.config.CertFingerprint != "" {
    48  			conState := c.ConnectionState()
    49  			for _, peercert := range conState.PeerCertificates {
    50  				if bytes.Equal(peercert.Signature[0:], []byte(cc.config.CertFingerprint)) {
    51  					return c, nil
    52  				}
    53  			}
    54  			return nil, fmt.Errorf("certificate pinning failed")
    55  		}
    56  		return c, nil
    57  	}
    58  }
    59  
    60  // Get the public key from central. PublicKeyURI can be relative to the base URL
    61  // so we need to parse and make sure finally URL is resolved.
    62  func (cc *CentralClient) PublicKey() ([]byte, error) {
    63  	target, err := cc.toURL(cc.config.PublicKeyURI)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	log.Debug("downloading public key", "url", target)
    68  	buf := new(bytes.Buffer)
    69  	if err := cc.download(target, buf); err != nil {
    70  		return nil, err
    71  	}
    72  	return buf.Bytes(), nil
    73  }
    74  
    75  // retrieve plugin signature
    76  func (cc *CentralClient) PluginSignature(definition *PluginDefinition) ([]byte, error) {
    77  	target, err := cc.toURLFromTemplate(cc.config.PluginSigPathTemplate, definition)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  	log.Debug("downloading plugin signature file", "url", target)
    82  	buf := new(bytes.Buffer)
    83  	if err := cc.download(target, buf); err != nil {
    84  		return nil, err
    85  	}
    86  	return buf.Bytes(), nil
    87  }
    88  
    89  // retrieve plugin distribution file
    90  func (cc *CentralClient) PluginDistribution(definition *PluginDefinition, outFilePath string) error {
    91  	target, err := cc.toURLFromTemplate(cc.config.PluginDistPathTemplate, definition)
    92  	if err != nil {
    93  		return err
    94  	}
    95  	log.Debug("downloading plugin zip file", "url", target)
    96  	outFile, err := os.Create(outFilePath)
    97  	if err != nil {
    98  		return err
    99  	}
   100  	defer func() {
   101  		_ = outFile.Close()
   102  	}()
   103  	return cc.download(target, outFile)
   104  }
   105  
   106  // perform HTTP GET
   107  //
   108  // caller needs to close the reader
   109  func (cc *CentralClient) get(target string) (io.ReadCloser, error) {
   110  	if err := isValidTargetURL(cc.config.BaseURL, target); err != nil {
   111  		return nil, err
   112  	}
   113  	res, err := cc.httpClient.Get(target)
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  	if res.StatusCode != http.StatusOK {
   118  		defer func() {
   119  			_ = res.Body.Close()
   120  		}()
   121  		data, _ := ioutil.ReadAll(res.Body)
   122  		return nil, fmt.Errorf("HTTP GET error: code=%d, status=%s, body=%s", res.StatusCode, res.Status, string(data))
   123  	}
   124  	return res.Body, nil
   125  }
   126  
   127  // return full URL using config.BaseURL
   128  func (cc *CentralClient) toURL(relativePath string) (string, error) {
   129  	base, err := url.Parse(cc.config.BaseURL)
   130  	if err != nil {
   131  		return "", err
   132  	}
   133  	u, err := base.Parse(relativePath)
   134  	if err != nil {
   135  		return "", err
   136  	}
   137  	return u.String(), nil
   138  }
   139  
   140  // return full URL using config.BaseURL from given template
   141  func (cc *CentralClient) toURLFromTemplate(pathTemplate string, definition *PluginDefinition) (string, error) {
   142  	t, err := template.New("").Parse(pathTemplate)
   143  	if err != nil {
   144  		return "", err
   145  	}
   146  	path := new(bytes.Buffer)
   147  	if err := t.Execute(path, struct {
   148  		Name    string
   149  		Version string
   150  		OS      string
   151  		Arch    string
   152  	}{
   153  		Name:    definition.Name,
   154  		Version: string(definition.Version),
   155  		OS:      runtime.GOOS,
   156  		Arch:    runtime.GOARCH,
   157  	}); err != nil {
   158  		return "", err
   159  	}
   160  	return cc.toURL(path.String())
   161  }
   162  
   163  // peform http GET to the target URL and write output to out
   164  func (cc *CentralClient) download(target string, out io.Writer) (err error) {
   165  	defer func(start time.Time) {
   166  		log.Debug("download completed", "url", target, "err", err, "took", time.Since(start))
   167  	}(time.Now())
   168  	var readCloser io.ReadCloser
   169  	readCloser, err = cc.get(target)
   170  	if err != nil {
   171  		return
   172  	}
   173  	defer func() {
   174  		_ = readCloser.Close()
   175  	}()
   176  	_, err = io.Copy(out, readCloser)
   177  	return err
   178  }
   179  
   180  // An adapter function for tls.Dial with CA verification & SSL Pinning support.
   181  type Dialer func(network, addr string) (net.Conn, error)
   182  
   183  // Validate the target url is well formed and match base.
   184  func isValidTargetURL(base string, target string) error {
   185  	u, err := url.Parse(target)
   186  	if err != nil {
   187  		return err
   188  	}
   189  	t, err := url.Parse(base)
   190  	if err != nil {
   191  		return err
   192  	}
   193  	if strings.Compare(t.Host, u.Host) != 0 || strings.Compare(t.Scheme, u.Scheme) != 0 {
   194  		return fmt.Errorf("target host doesnt match base host")
   195  	}
   196  	return nil
   197  }