istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/wasm/httpfetcher.go (about) 1 // Copyright Istio Authors 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package wasm 16 17 import ( 18 "archive/tar" 19 "bytes" 20 "compress/gzip" 21 "context" 22 "crypto/tls" 23 "fmt" 24 "io" 25 "net/http" 26 "time" 27 28 "istio.io/istio/pkg/backoff" 29 ) 30 31 var ( 32 // Referred to https://en.wikipedia.org/wiki/Tar_(computing)#UStar_format 33 tarMagicNumber = []byte{0x75, 0x73, 0x74, 0x61, 0x72} 34 // Referred to https://en.wikipedia.org/wiki/Gzip#File_format 35 gzMagicNumber = []byte{0x1f, 0x8b} 36 ) 37 38 // HTTPFetcher fetches remote wasm module with HTTP get. 39 type HTTPFetcher struct { 40 client *http.Client 41 insecureClient *http.Client 42 initialBackoff time.Duration 43 requestMaxRetry int 44 } 45 46 // NewHTTPFetcher create a new HTTP remote wasm module fetcher. 47 // requestTimeout is a timeout for each HTTP/HTTPS request. 48 // requestMaxRetry is # of maximum retries of HTTP/HTTPS requests. 49 func NewHTTPFetcher(requestTimeout time.Duration, requestMaxRetry int) *HTTPFetcher { 50 if requestTimeout == 0 { 51 requestTimeout = 5 * time.Second 52 } 53 transport := http.DefaultTransport.(*http.Transport).Clone() 54 // nolint: gosec 55 // This is only when a user explicitly sets a flag to enable insecure mode 56 transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 57 return &HTTPFetcher{ 58 client: &http.Client{ 59 Timeout: requestTimeout, 60 }, 61 insecureClient: &http.Client{ 62 Timeout: requestTimeout, 63 Transport: transport, 64 }, 65 initialBackoff: time.Millisecond * 500, 66 requestMaxRetry: requestMaxRetry, 67 } 68 } 69 70 // Fetch downloads a wasm module with HTTP get. 71 func (f *HTTPFetcher) Fetch(ctx context.Context, url string, allowInsecure bool) ([]byte, error) { 72 c := f.client 73 if allowInsecure { 74 c = f.insecureClient 75 } 76 attempts := 0 77 o := backoff.DefaultOption() 78 o.InitialInterval = f.initialBackoff 79 b := backoff.NewExponentialBackOff(o) 80 var lastError error 81 for attempts < f.requestMaxRetry { 82 attempts++ 83 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 84 if err != nil { 85 wasmLog.Debugf("wasm module download request failed: %v", err) 86 return nil, err 87 } 88 resp, err := c.Do(req) 89 if err != nil { 90 lastError = err 91 wasmLog.Debugf("wasm module download request failed: %v", err) 92 if ctx.Err() != nil { 93 // If there is context timeout, exit this loop. 94 return nil, fmt.Errorf("wasm module download failed after %v attempts, last error: %v", attempts, lastError) 95 } 96 time.Sleep(b.NextBackOff()) 97 continue 98 } 99 if resp.StatusCode == http.StatusOK { 100 // Limit wasm module to 256mb; in reality it must be much smaller 101 body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024*256)) 102 if err != nil { 103 return nil, err 104 } 105 err = resp.Body.Close() 106 if err != nil { 107 wasmLog.Infof("wasm server connection is not closed: %v", err) 108 } 109 return unboxIfPossible(body), err 110 } 111 lastError = fmt.Errorf("wasm module download request failed: status code %v", resp.StatusCode) 112 if retryable(resp.StatusCode) { 113 // Limit wasm module to 256mb; in reality it must be much smaller 114 body, err := io.ReadAll(io.LimitReader(resp.Body, 1024*1024*256)) 115 if err != nil { 116 return nil, err 117 } 118 wasmLog.Debugf("wasm module download failed: status code %v, body %v", resp.StatusCode, string(body)) 119 err = resp.Body.Close() 120 if err != nil { 121 wasmLog.Infof("wasm server connection is not closed: %v", err) 122 } 123 time.Sleep(b.NextBackOff()) 124 continue 125 } 126 err = resp.Body.Close() 127 if err != nil { 128 wasmLog.Infof("wasm server connection is not closed: %v", err) 129 } 130 break 131 } 132 return nil, fmt.Errorf("wasm module download failed after %v attempts, last error: %v", attempts, lastError) 133 } 134 135 func retryable(code int) bool { 136 return code >= 500 && 137 !(code == http.StatusNotImplemented || 138 code == http.StatusHTTPVersionNotSupported || 139 code == http.StatusNetworkAuthenticationRequired) 140 } 141 142 func isPosixTar(b []byte) bool { 143 return len(b) > 262 && bytes.Equal(b[257:262], tarMagicNumber) 144 } 145 146 // wasm plugin should be the only file in the tarball. 147 func getFirstFileFromTar(b []byte) []byte { 148 buf := bytes.NewBuffer(b) 149 150 // Limit wasm module to 256mb; in reality it must be much smaller 151 tr := tar.NewReader(io.LimitReader(buf, 1024*1024*256)) 152 153 h, err := tr.Next() 154 if err != nil { 155 return nil 156 } 157 158 ret := make([]byte, h.Size) 159 _, err = io.ReadFull(tr, ret) 160 if err != nil { 161 return nil 162 } 163 return ret 164 } 165 166 func isGZ(b []byte) bool { 167 return len(b) > 2 && bytes.Equal(b[:2], gzMagicNumber) 168 } 169 170 func getFileFromGZ(b []byte) []byte { 171 buf := bytes.NewBuffer(b) 172 173 zr, err := gzip.NewReader(buf) 174 if err != nil { 175 return nil 176 } 177 178 ret, err := io.ReadAll(zr) 179 if err != nil { 180 return nil 181 } 182 return ret 183 } 184 185 // Just do the best effort. 186 // If an error is encountered, just return the original bytes. 187 // Errors will be handled upper layers. 188 func unboxIfPossible(origin []byte) []byte { 189 b := origin 190 for { 191 if isValidWasmBinary(b) { 192 return b 193 } else if isGZ(b) { 194 if b = getFileFromGZ(b); b == nil { 195 return origin 196 } 197 } else if isPosixTar(b) { 198 if b = getFirstFileFromTar(b); b == nil { 199 return origin 200 } 201 } else { 202 return origin 203 } 204 } 205 }