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 }