github.com/Finschia/finschia-sdk@v0.48.1/server/grpc/grpc_web_test.go (about)

     1  //go:build norace
     2  // +build norace
     3  
     4  package grpc_test
     5  
     6  import (
     7  	"bufio"
     8  	"bytes"
     9  	"encoding/base64"
    10  	"encoding/binary"
    11  	"fmt"
    12  	"io"
    13  	"net/http"
    14  	"net/textproto"
    15  	"strconv"
    16  	"strings"
    17  	"testing"
    18  
    19  	"github.com/golang/protobuf/proto"
    20  	"github.com/stretchr/testify/require"
    21  	"github.com/stretchr/testify/suite"
    22  	"google.golang.org/grpc/codes"
    23  
    24  	"github.com/Finschia/finschia-sdk/client/grpc/tmservice"
    25  	"github.com/Finschia/finschia-sdk/codec"
    26  	cryptotypes "github.com/Finschia/finschia-sdk/crypto/types"
    27  	"github.com/Finschia/finschia-sdk/testutil/network"
    28  	banktypes "github.com/Finschia/finschia-sdk/x/bank/types"
    29  )
    30  
    31  // https://github.com/improbable-eng/grpc-web/blob/master/go/grpcweb/wrapper_test.go used as a reference
    32  // to setup grpcRequest config.
    33  
    34  const grpcWebContentType = "application/grpc-web"
    35  
    36  type GRPCWebTestSuite struct {
    37  	suite.Suite
    38  
    39  	cfg      network.Config
    40  	network  *network.Network
    41  	protoCdc *codec.ProtoCodec
    42  }
    43  
    44  func (s *GRPCWebTestSuite) SetupSuite() {
    45  	s.T().Log("setting up integration test suite")
    46  
    47  	cfg := network.DefaultConfig()
    48  	cfg.NumValidators = 1
    49  	s.cfg = cfg
    50  	s.network = network.New(s.T(), s.cfg)
    51  	s.Require().NotNil(s.network)
    52  
    53  	_, err := s.network.WaitForHeight(2)
    54  	s.Require().NoError(err)
    55  
    56  	s.protoCdc = codec.NewProtoCodec(s.cfg.InterfaceRegistry)
    57  }
    58  
    59  func (s *GRPCWebTestSuite) TearDownSuite() {
    60  	s.T().Log("tearing down integration test suite")
    61  	s.network.Cleanup()
    62  }
    63  
    64  func (s *GRPCWebTestSuite) Test_Latest_Validators() {
    65  	val := s.network.Validators[0]
    66  	for _, contentType := range []string{grpcWebContentType} {
    67  		headers, trailers, responses, err := s.makeGrpcRequest(
    68  			"/lbm.base.ostracon.v1.Service/GetLatestValidatorSet",
    69  			headerWithFlag(),
    70  			serializeProtoMessages([]proto.Message{&tmservice.GetLatestValidatorSetRequest{}}), false)
    71  
    72  		s.Require().NoError(err)
    73  		s.Require().Equal(1, len(responses))
    74  		s.assertTrailerGrpcCode(trailers, codes.OK, "")
    75  		s.assertContentTypeSet(headers, contentType)
    76  		var valsSet tmservice.GetLatestValidatorSetResponse
    77  		err = s.protoCdc.Unmarshal(responses[0], &valsSet)
    78  		s.Require().NoError(err)
    79  		pubKey, ok := valsSet.Validators[0].PubKey.GetCachedValue().(cryptotypes.PubKey)
    80  		s.Require().Equal(true, ok)
    81  		s.Require().Equal(pubKey, val.PubKey)
    82  	}
    83  }
    84  
    85  func (s *GRPCWebTestSuite) Test_Total_Supply() {
    86  	for _, contentType := range []string{grpcWebContentType} {
    87  		headers, trailers, responses, err := s.makeGrpcRequest(
    88  			"/cosmos.bank.v1beta1.Query/TotalSupply",
    89  			headerWithFlag(),
    90  			serializeProtoMessages([]proto.Message{&banktypes.QueryTotalSupplyRequest{}}), false)
    91  
    92  		s.Require().NoError(err)
    93  		s.Require().Equal(1, len(responses))
    94  		s.assertTrailerGrpcCode(trailers, codes.OK, "")
    95  		s.assertContentTypeSet(headers, contentType)
    96  		var totalSupply banktypes.QueryTotalSupplyResponse
    97  		_ = s.protoCdc.Unmarshal(responses[0], &totalSupply)
    98  	}
    99  }
   100  
   101  func (s *GRPCWebTestSuite) assertContentTypeSet(headers http.Header, contentType string) {
   102  	s.Require().Equal(contentType, headers.Get("content-type"), `Expected there to be content-type=%v`, contentType)
   103  }
   104  
   105  func (s *GRPCWebTestSuite) assertTrailerGrpcCode(trailers Trailer, code codes.Code, desc string) {
   106  	s.Require().NotEmpty(trailers.Get("grpc-status"), "grpc-status must not be empty in trailers")
   107  	statusCode, err := strconv.Atoi(trailers.Get("grpc-status"))
   108  	s.Require().NoError(err, "no error parsing grpc-status")
   109  	s.Require().EqualValues(code, statusCode, "grpc-status must match expected code")
   110  	s.Require().EqualValues(desc, trailers.Get("grpc-message"), "grpc-message is expected to match")
   111  }
   112  
   113  func serializeProtoMessages(messages []proto.Message) [][]byte {
   114  	out := [][]byte{}
   115  	for _, m := range messages {
   116  		b, _ := proto.Marshal(m)
   117  		out = append(out, b)
   118  	}
   119  	return out
   120  }
   121  
   122  func (s *GRPCWebTestSuite) makeRequest(
   123  	verb string, method string, headers http.Header, body io.Reader, isText bool,
   124  ) (*http.Response, error) {
   125  	val := s.network.Validators[0]
   126  	contentType := "application/grpc-web"
   127  	if isText {
   128  		// base64 encode the body
   129  		encodedBody := &bytes.Buffer{}
   130  		encoder := base64.NewEncoder(base64.StdEncoding, encodedBody)
   131  		_, err := io.Copy(encoder, body)
   132  		if err != nil {
   133  			return nil, err
   134  		}
   135  		err = encoder.Close()
   136  		if err != nil {
   137  			return nil, err
   138  		}
   139  		body = encodedBody
   140  		contentType = "application/grpc-web-text"
   141  	}
   142  
   143  	url := fmt.Sprintf("http://%s%s", val.AppConfig.GRPCWeb.Address, method)
   144  	req, err := http.NewRequest(verb, url, body)
   145  	s.Require().NoError(err, "failed creating a request")
   146  	req.Header = headers
   147  
   148  	req.Header.Set("Content-Type", contentType)
   149  	client := &http.Client{}
   150  	resp, err := client.Do(req)
   151  	return resp, err
   152  }
   153  
   154  func decodeMultipleBase64Chunks(b []byte) ([]byte, error) {
   155  	// grpc-web allows multiple base64 chunks: the implementation may send base64-encoded
   156  	// "chunks" with potential padding whenever the runtime needs to flush a byte buffer.
   157  	// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md
   158  	output := make([]byte, base64.StdEncoding.DecodedLen(len(b)))
   159  	outputEnd := 0
   160  
   161  	for inputEnd := 0; inputEnd < len(b); {
   162  		chunk := b[inputEnd:]
   163  		paddingIndex := bytes.IndexByte(chunk, '=')
   164  		if paddingIndex != -1 {
   165  			// find the consecutive =
   166  			for {
   167  				paddingIndex += 1
   168  				if paddingIndex >= len(chunk) || chunk[paddingIndex] != '=' {
   169  					break
   170  				}
   171  			}
   172  			chunk = chunk[:paddingIndex]
   173  		}
   174  		inputEnd += len(chunk)
   175  
   176  		n, err := base64.StdEncoding.Decode(output[outputEnd:], chunk)
   177  		if err != nil {
   178  			return nil, err
   179  		}
   180  		outputEnd += n
   181  	}
   182  	return output[:outputEnd], nil
   183  }
   184  
   185  func (s *GRPCWebTestSuite) makeGrpcRequest(
   186  	method string, reqHeaders http.Header, requestMessages [][]byte, isText bool,
   187  ) (headers http.Header, trailers Trailer, responseMessages [][]byte, err error) {
   188  	writer := new(bytes.Buffer)
   189  	for _, msgBytes := range requestMessages {
   190  		grpcPreamble := []byte{0, 0, 0, 0, 0}
   191  		binary.BigEndian.PutUint32(grpcPreamble[1:], uint32(len(msgBytes)))
   192  		writer.Write(grpcPreamble)
   193  		writer.Write(msgBytes)
   194  	}
   195  	resp, err := s.makeRequest("POST", method, reqHeaders, writer, isText)
   196  	if err != nil {
   197  		return nil, Trailer{}, nil, err
   198  	}
   199  	defer resp.Body.Close()
   200  	contents, err := io.ReadAll(resp.Body)
   201  	if err != nil {
   202  		return nil, Trailer{}, nil, err
   203  	}
   204  
   205  	if isText {
   206  		contents, err = decodeMultipleBase64Chunks(contents)
   207  		if err != nil {
   208  			return nil, Trailer{}, nil, err
   209  		}
   210  	}
   211  
   212  	reader := bytes.NewReader(contents)
   213  	for {
   214  		grpcPreamble := []byte{0, 0, 0, 0, 0}
   215  		readCount, err := reader.Read(grpcPreamble)
   216  		if err == io.EOF {
   217  			break
   218  		}
   219  		if readCount != 5 || err != nil {
   220  			return nil, Trailer{}, nil, fmt.Errorf("Unexpected end of body in preamble: %v", err)
   221  		}
   222  		payloadLength := binary.BigEndian.Uint32(grpcPreamble[1:])
   223  		payloadBytes := make([]byte, payloadLength)
   224  
   225  		readCount, err = reader.Read(payloadBytes)
   226  		if uint32(readCount) != payloadLength || err != nil {
   227  			return nil, Trailer{}, nil, fmt.Errorf("Unexpected end of msg: %v", err)
   228  		}
   229  		if grpcPreamble[0]&(1<<7) == (1 << 7) { // MSB signifies the trailer parser
   230  			trailers = readTrailersFromBytes(s.T(), payloadBytes)
   231  		} else {
   232  			responseMessages = append(responseMessages, payloadBytes)
   233  		}
   234  	}
   235  	return resp.Header, trailers, responseMessages, nil
   236  }
   237  
   238  func readTrailersFromBytes(t *testing.T, dataBytes []byte) Trailer {
   239  	bufferReader := bytes.NewBuffer(dataBytes)
   240  	tp := textproto.NewReader(bufio.NewReader(bufferReader))
   241  
   242  	// First, read bytes as MIME headers.
   243  	// However, it normalizes header names by textproto.CanonicalMIMEHeaderKey.
   244  	// In the next step, replace header names by raw one.
   245  	mimeHeader, err := tp.ReadMIMEHeader()
   246  	if err == nil {
   247  		return Trailer{}
   248  	}
   249  
   250  	trailers := make(http.Header)
   251  	bufferReader = bytes.NewBuffer(dataBytes)
   252  	tp = textproto.NewReader(bufio.NewReader(bufferReader))
   253  
   254  	// Second, replace header names because gRPC Web trailer names must be lower-case.
   255  	for {
   256  		line, err := tp.ReadLine()
   257  		if err == io.EOF {
   258  			break
   259  		}
   260  		require.NoError(t, err, "failed to read header line")
   261  
   262  		i := strings.IndexByte(line, ':')
   263  		if i == -1 {
   264  			require.FailNow(t, "malformed header", line)
   265  		}
   266  		key := line[:i]
   267  		if vv, ok := mimeHeader[textproto.CanonicalMIMEHeaderKey(key)]; ok {
   268  			trailers[key] = vv
   269  		}
   270  	}
   271  	return HTTPTrailerToGrpcWebTrailer(trailers)
   272  }
   273  
   274  func headerWithFlag(flags ...string) http.Header {
   275  	h := http.Header{}
   276  	for _, f := range flags {
   277  		h.Set(f, "true")
   278  	}
   279  	return h
   280  }
   281  
   282  type Trailer struct {
   283  	trailer
   284  }
   285  
   286  func HTTPTrailerToGrpcWebTrailer(httpTrailer http.Header) Trailer {
   287  	return Trailer{trailer{httpTrailer}}
   288  }
   289  
   290  // gRPC-Web spec says that must use lower-case header/trailer names.
   291  // See "HTTP wire protocols" section in
   292  // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2
   293  type trailer struct {
   294  	http.Header
   295  }
   296  
   297  func (t trailer) Add(key, value string) {
   298  	key = strings.ToLower(key)
   299  	t.Header[key] = append(t.Header[key], value)
   300  }
   301  
   302  func (t trailer) Get(key string) string {
   303  	if t.Header == nil {
   304  		return ""
   305  	}
   306  	v := t.Header[key]
   307  	if len(v) == 0 {
   308  		return ""
   309  	}
   310  	return v[0]
   311  }
   312  
   313  func TestGRPCWebTestSuite(t *testing.T) {
   314  	suite.Run(t, new(GRPCWebTestSuite))
   315  }