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  }