mirror of
https://github.com/taigrr/wasm-experiments
synced 2025-01-18 04:03:21 -08:00
302 lines
9.7 KiB
Go
302 lines
9.7 KiB
Go
/*
|
|
*
|
|
* Copyright 2014 gRPC authors.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*
|
|
*/
|
|
|
|
package transport
|
|
|
|
import (
|
|
"net"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/net/context"
|
|
"golang.org/x/net/http2/hpack"
|
|
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/credentials"
|
|
"google.golang.org/grpc/keepalive"
|
|
"google.golang.org/grpc/metadata"
|
|
"google.golang.org/grpc/peer"
|
|
"google.golang.org/grpc/stats"
|
|
"google.golang.org/grpc/status"
|
|
)
|
|
|
|
// http2Client implements the ClientTransport interface with HTTP2.
|
|
type http2Client struct {
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
ctxDone <-chan struct{} // Cache the ctx.Done() chan.
|
|
userAgent string
|
|
md interface{}
|
|
conn net.Conn // underlying communication channel
|
|
loopy *loopyWriter
|
|
remoteAddr net.Addr
|
|
localAddr net.Addr
|
|
authInfo credentials.AuthInfo // auth info about the connection
|
|
|
|
readerDone chan struct{} // sync point to enable testing.
|
|
writerDone chan struct{} // sync point to enable testing.
|
|
// goAway is closed to notify the upper layer (i.e., addrConn.transportMonitor)
|
|
// that the server sent GoAway on this transport.
|
|
goAway chan struct{}
|
|
// awakenKeepalive is used to wake up keepalive when after it has gone dormant.
|
|
awakenKeepalive chan struct{}
|
|
|
|
framer *framer
|
|
// controlBuf delivers all the control related tasks (e.g., window
|
|
// updates, reset streams, and various settings) to the controller.
|
|
controlBuf *controlBuffer
|
|
fc *trInFlow
|
|
// The scheme used: https if TLS is on, http otherwise.
|
|
scheme string
|
|
|
|
isSecure bool
|
|
|
|
creds []credentials.PerRPCCredentials
|
|
|
|
// Boolean to keep track of reading activity on transport.
|
|
// 1 is true and 0 is false.
|
|
activity uint32 // Accessed atomically.
|
|
kp keepalive.ClientParameters
|
|
keepaliveEnabled bool
|
|
|
|
statsHandler stats.Handler
|
|
|
|
initialWindowSize int32
|
|
|
|
bdpEst *bdpEstimator
|
|
// onSuccess is a callback that client transport calls upon
|
|
// receiving server preface to signal that a succefull HTTP2
|
|
// connection was established.
|
|
onSuccess func()
|
|
|
|
maxConcurrentStreams uint32
|
|
streamQuota int64
|
|
streamsQuotaAvailable chan struct{}
|
|
waitingStreams uint32
|
|
nextID uint32
|
|
|
|
mu sync.Mutex // guard the following variables
|
|
state transportState
|
|
activeStreams map[uint32]*Stream
|
|
// prevGoAway ID records the Last-Stream-ID in the previous GOAway frame.
|
|
prevGoAwayID uint32
|
|
// goAwayReason records the http2.ErrCode and debug data received with the
|
|
// GoAway frame.
|
|
goAwayReason GoAwayReason
|
|
|
|
// Fields below are for channelz metric collection.
|
|
channelzID int64 // channelz unique identification number
|
|
czmu sync.RWMutex
|
|
kpCount int64
|
|
// The number of streams that have started, including already finished ones.
|
|
streamsStarted int64
|
|
// The number of streams that have ended successfully by receiving EoS bit set
|
|
// frame from server.
|
|
streamsSucceeded int64
|
|
streamsFailed int64
|
|
lastStreamCreated time.Time
|
|
msgSent int64
|
|
msgRecv int64
|
|
lastMsgSent time.Time
|
|
lastMsgRecv time.Time
|
|
}
|
|
|
|
func dial(ctx context.Context, fn func(context.Context, string) (net.Conn, error), addr string) (net.Conn, error) {
|
|
if fn != nil {
|
|
return fn(ctx, addr)
|
|
}
|
|
return dialContext(ctx, "tcp", addr)
|
|
}
|
|
|
|
func isTemporary(err error) bool {
|
|
switch err := err.(type) {
|
|
case interface {
|
|
Temporary() bool
|
|
}:
|
|
return err.Temporary()
|
|
case interface {
|
|
Timeout() bool
|
|
}:
|
|
// Timeouts may be resolved upon retry, and are thus treated as
|
|
// temporary.
|
|
return err.Timeout()
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (t *http2Client) getPeer() *peer.Peer {
|
|
pr := &peer.Peer{
|
|
Addr: t.remoteAddr,
|
|
}
|
|
// Attach Auth info if there is any.
|
|
if t.authInfo != nil {
|
|
pr.AuthInfo = t.authInfo
|
|
}
|
|
return pr
|
|
}
|
|
|
|
func (t *http2Client) createHeaderFields(ctx context.Context, callHdr *CallHdr) ([]hpack.HeaderField, error) {
|
|
aud := t.createAudience(callHdr)
|
|
authData, err := t.getTrAuthData(ctx, aud)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
callAuthData, err := t.getCallAuthData(ctx, aud, callHdr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// TODO(mmukhi): Benchmark if the performance gets better if count the metadata and other header fields
|
|
// first and create a slice of that exact size.
|
|
// Make the slice of certain predictable size to reduce allocations made by append.
|
|
hfLen := 7 // :method, :scheme, :path, :authority, content-type, user-agent, te
|
|
hfLen += len(authData) + len(callAuthData)
|
|
headerFields := make([]hpack.HeaderField, 0, hfLen)
|
|
headerFields = append(headerFields, hpack.HeaderField{Name: ":method", Value: "POST"})
|
|
headerFields = append(headerFields, hpack.HeaderField{Name: ":scheme", Value: t.scheme})
|
|
headerFields = append(headerFields, hpack.HeaderField{Name: ":path", Value: callHdr.Method})
|
|
headerFields = append(headerFields, hpack.HeaderField{Name: ":authority", Value: callHdr.Host})
|
|
headerFields = append(headerFields, hpack.HeaderField{Name: "content-type", Value: contentType(callHdr.ContentSubtype)})
|
|
headerFields = append(headerFields, hpack.HeaderField{Name: "user-agent", Value: t.userAgent})
|
|
headerFields = append(headerFields, hpack.HeaderField{Name: "te", Value: "trailers"})
|
|
|
|
if callHdr.SendCompress != "" {
|
|
headerFields = append(headerFields, hpack.HeaderField{Name: "grpc-encoding", Value: callHdr.SendCompress})
|
|
}
|
|
if dl, ok := ctx.Deadline(); ok {
|
|
// Send out timeout regardless its value. The server can detect timeout context by itself.
|
|
// TODO(mmukhi): Perhaps this field should be updated when actually writing out to the wire.
|
|
timeout := dl.Sub(time.Now())
|
|
headerFields = append(headerFields, hpack.HeaderField{Name: "grpc-timeout", Value: encodeTimeout(timeout)})
|
|
}
|
|
for k, v := range authData {
|
|
headerFields = append(headerFields, hpack.HeaderField{Name: k, Value: encodeMetadataHeader(k, v)})
|
|
}
|
|
for k, v := range callAuthData {
|
|
headerFields = append(headerFields, hpack.HeaderField{Name: k, Value: encodeMetadataHeader(k, v)})
|
|
}
|
|
if b := stats.OutgoingTags(ctx); b != nil {
|
|
headerFields = append(headerFields, hpack.HeaderField{Name: "grpc-tags-bin", Value: encodeBinHeader(b)})
|
|
}
|
|
if b := stats.OutgoingTrace(ctx); b != nil {
|
|
headerFields = append(headerFields, hpack.HeaderField{Name: "grpc-trace-bin", Value: encodeBinHeader(b)})
|
|
}
|
|
|
|
if md, added, ok := metadata.FromOutgoingContextRaw(ctx); ok {
|
|
var k string
|
|
for _, vv := range added {
|
|
for i, v := range vv {
|
|
if i%2 == 0 {
|
|
k = v
|
|
continue
|
|
}
|
|
// HTTP doesn't allow you to set pseudoheaders after non pseudoheaders were set.
|
|
if isReservedHeader(k) {
|
|
continue
|
|
}
|
|
headerFields = append(headerFields, hpack.HeaderField{Name: strings.ToLower(k), Value: encodeMetadataHeader(k, v)})
|
|
}
|
|
}
|
|
for k, vv := range md {
|
|
// HTTP doesn't allow you to set pseudoheaders after non pseudoheaders were set.
|
|
if isReservedHeader(k) {
|
|
continue
|
|
}
|
|
for _, v := range vv {
|
|
headerFields = append(headerFields, hpack.HeaderField{Name: k, Value: encodeMetadataHeader(k, v)})
|
|
}
|
|
}
|
|
}
|
|
if md, ok := t.md.(*metadata.MD); ok {
|
|
for k, vv := range *md {
|
|
if isReservedHeader(k) {
|
|
continue
|
|
}
|
|
for _, v := range vv {
|
|
headerFields = append(headerFields, hpack.HeaderField{Name: k, Value: encodeMetadataHeader(k, v)})
|
|
}
|
|
}
|
|
}
|
|
return headerFields, nil
|
|
}
|
|
|
|
func (t *http2Client) createAudience(callHdr *CallHdr) string {
|
|
// Create an audience string only if needed.
|
|
if len(t.creds) == 0 && callHdr.Creds == nil {
|
|
return ""
|
|
}
|
|
// Construct URI required to get auth request metadata.
|
|
// Omit port if it is the default one.
|
|
host := strings.TrimSuffix(callHdr.Host, ":443")
|
|
pos := strings.LastIndex(callHdr.Method, "/")
|
|
if pos == -1 {
|
|
pos = len(callHdr.Method)
|
|
}
|
|
return "https://" + host + callHdr.Method[:pos]
|
|
}
|
|
|
|
func (t *http2Client) getTrAuthData(ctx context.Context, audience string) (map[string]string, error) {
|
|
authData := map[string]string{}
|
|
for _, c := range t.creds {
|
|
data, err := c.GetRequestMetadata(ctx, audience)
|
|
if err != nil {
|
|
if _, ok := status.FromError(err); ok {
|
|
return nil, err
|
|
}
|
|
|
|
return nil, streamErrorf(codes.Unauthenticated, "transport: %v", err)
|
|
}
|
|
for k, v := range data {
|
|
// Capital header names are illegal in HTTP/2.
|
|
k = strings.ToLower(k)
|
|
authData[k] = v
|
|
}
|
|
}
|
|
return authData, nil
|
|
}
|
|
|
|
func (t *http2Client) getCallAuthData(ctx context.Context, audience string, callHdr *CallHdr) (map[string]string, error) {
|
|
callAuthData := map[string]string{}
|
|
// Check if credentials.PerRPCCredentials were provided via call options.
|
|
// Note: if these credentials are provided both via dial options and call
|
|
// options, then both sets of credentials will be applied.
|
|
if callCreds := callHdr.Creds; callCreds != nil {
|
|
if !t.isSecure && callCreds.RequireTransportSecurity() {
|
|
return nil, streamErrorf(codes.Unauthenticated, "transport: cannot send secure credentials on an insecure connection")
|
|
}
|
|
data, err := callCreds.GetRequestMetadata(ctx, audience)
|
|
if err != nil {
|
|
return nil, streamErrorf(codes.Internal, "transport: %v", err)
|
|
}
|
|
for k, v := range data {
|
|
// Capital header names are illegal in HTTP/2
|
|
k = strings.ToLower(k)
|
|
callAuthData[k] = v
|
|
}
|
|
}
|
|
return callAuthData, nil
|
|
}
|
|
|
|
func (t *http2Client) Error() <-chan struct{} {
|
|
return t.ctx.Done()
|
|
}
|
|
|
|
func (t *http2Client) GoAway() <-chan struct{} {
|
|
return t.goAway
|
|
}
|