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 }