blob: 896057e1e1e108304604e0ac7e27fd4add0c9204 [file] [log] [blame]
khenaidoo59ce9dd2019-11-11 13:05:32 -05001package runtime
2
3import (
4 "context"
5 "encoding/base64"
6 "fmt"
7 "net"
8 "net/http"
9 "net/textproto"
10 "strconv"
11 "strings"
12 "time"
13
14 "google.golang.org/grpc/codes"
15 "google.golang.org/grpc/grpclog"
16 "google.golang.org/grpc/metadata"
17 "google.golang.org/grpc/status"
18)
19
20// MetadataHeaderPrefix is the http prefix that represents custom metadata
21// parameters to or from a gRPC call.
22const MetadataHeaderPrefix = "Grpc-Metadata-"
23
24// MetadataPrefix is prepended to permanent HTTP header keys (as specified
25// by the IANA) when added to the gRPC context.
26const MetadataPrefix = "grpcgateway-"
27
28// MetadataTrailerPrefix is prepended to gRPC metadata as it is converted to
29// HTTP headers in a response handled by grpc-gateway
30const MetadataTrailerPrefix = "Grpc-Trailer-"
31
32const metadataGrpcTimeout = "Grpc-Timeout"
33const metadataHeaderBinarySuffix = "-Bin"
34
35const xForwardedFor = "X-Forwarded-For"
36const xForwardedHost = "X-Forwarded-Host"
37
38var (
39 // DefaultContextTimeout is used for gRPC call context.WithTimeout whenever a Grpc-Timeout inbound
40 // header isn't present. If the value is 0 the sent `context` will not have a timeout.
41 DefaultContextTimeout = 0 * time.Second
42)
43
44func decodeBinHeader(v string) ([]byte, error) {
45 if len(v)%4 == 0 {
46 // Input was padded, or padding was not necessary.
47 return base64.StdEncoding.DecodeString(v)
48 }
49 return base64.RawStdEncoding.DecodeString(v)
50}
51
52/*
53AnnotateContext adds context information such as metadata from the request.
54
55At a minimum, the RemoteAddr is included in the fashion of "X-Forwarded-For",
56except that the forwarded destination is not another HTTP service but rather
57a gRPC service.
58*/
59func AnnotateContext(ctx context.Context, mux *ServeMux, req *http.Request) (context.Context, error) {
60 var pairs []string
61 timeout := DefaultContextTimeout
62 if tm := req.Header.Get(metadataGrpcTimeout); tm != "" {
63 var err error
64 timeout, err = timeoutDecode(tm)
65 if err != nil {
66 return nil, status.Errorf(codes.InvalidArgument, "invalid grpc-timeout: %s", tm)
67 }
68 }
69
70 for key, vals := range req.Header {
71 for _, val := range vals {
72 key = textproto.CanonicalMIMEHeaderKey(key)
73 // For backwards-compatibility, pass through 'authorization' header with no prefix.
74 if key == "Authorization" {
75 pairs = append(pairs, "authorization", val)
76 }
77 if h, ok := mux.incomingHeaderMatcher(key); ok {
78 // Handles "-bin" metadata in grpc, since grpc will do another base64
79 // encode before sending to server, we need to decode it first.
80 if strings.HasSuffix(key, metadataHeaderBinarySuffix) {
81 b, err := decodeBinHeader(val)
82 if err != nil {
83 return nil, status.Errorf(codes.InvalidArgument, "invalid binary header %s: %s", key, err)
84 }
85
86 val = string(b)
87 }
88 pairs = append(pairs, h, val)
89 }
90 }
91 }
92 if host := req.Header.Get(xForwardedHost); host != "" {
93 pairs = append(pairs, strings.ToLower(xForwardedHost), host)
94 } else if req.Host != "" {
95 pairs = append(pairs, strings.ToLower(xForwardedHost), req.Host)
96 }
97
98 if addr := req.RemoteAddr; addr != "" {
99 if remoteIP, _, err := net.SplitHostPort(addr); err == nil {
100 if fwd := req.Header.Get(xForwardedFor); fwd == "" {
101 pairs = append(pairs, strings.ToLower(xForwardedFor), remoteIP)
102 } else {
103 pairs = append(pairs, strings.ToLower(xForwardedFor), fmt.Sprintf("%s, %s", fwd, remoteIP))
104 }
105 } else {
106 grpclog.Infof("invalid remote addr: %s", addr)
107 }
108 }
109
110 if timeout != 0 {
111 ctx, _ = context.WithTimeout(ctx, timeout)
112 }
113 if len(pairs) == 0 {
114 return ctx, nil
115 }
116 md := metadata.Pairs(pairs...)
117 for _, mda := range mux.metadataAnnotators {
118 md = metadata.Join(md, mda(ctx, req))
119 }
120 return metadata.NewOutgoingContext(ctx, md), nil
121}
122
123// ServerMetadata consists of metadata sent from gRPC server.
124type ServerMetadata struct {
125 HeaderMD metadata.MD
126 TrailerMD metadata.MD
127}
128
129type serverMetadataKey struct{}
130
131// NewServerMetadataContext creates a new context with ServerMetadata
132func NewServerMetadataContext(ctx context.Context, md ServerMetadata) context.Context {
133 return context.WithValue(ctx, serverMetadataKey{}, md)
134}
135
136// ServerMetadataFromContext returns the ServerMetadata in ctx
137func ServerMetadataFromContext(ctx context.Context) (md ServerMetadata, ok bool) {
138 md, ok = ctx.Value(serverMetadataKey{}).(ServerMetadata)
139 return
140}
141
142func timeoutDecode(s string) (time.Duration, error) {
143 size := len(s)
144 if size < 2 {
145 return 0, fmt.Errorf("timeout string is too short: %q", s)
146 }
147 d, ok := timeoutUnitToDuration(s[size-1])
148 if !ok {
149 return 0, fmt.Errorf("timeout unit is not recognized: %q", s)
150 }
151 t, err := strconv.ParseInt(s[:size-1], 10, 64)
152 if err != nil {
153 return 0, err
154 }
155 return d * time.Duration(t), nil
156}
157
158func timeoutUnitToDuration(u uint8) (d time.Duration, ok bool) {
159 switch u {
160 case 'H':
161 return time.Hour, true
162 case 'M':
163 return time.Minute, true
164 case 'S':
165 return time.Second, true
166 case 'm':
167 return time.Millisecond, true
168 case 'u':
169 return time.Microsecond, true
170 case 'n':
171 return time.Nanosecond, true
172 default:
173 }
174 return
175}
176
177// isPermanentHTTPHeader checks whether hdr belongs to the list of
178// permenant request headers maintained by IANA.
179// http://www.iana.org/assignments/message-headers/message-headers.xml
180func isPermanentHTTPHeader(hdr string) bool {
181 switch hdr {
182 case
183 "Accept",
184 "Accept-Charset",
185 "Accept-Language",
186 "Accept-Ranges",
187 "Authorization",
188 "Cache-Control",
189 "Content-Type",
190 "Cookie",
191 "Date",
192 "Expect",
193 "From",
194 "Host",
195 "If-Match",
196 "If-Modified-Since",
197 "If-None-Match",
198 "If-Schedule-Tag-Match",
199 "If-Unmodified-Since",
200 "Max-Forwards",
201 "Origin",
202 "Pragma",
203 "Referer",
204 "User-Agent",
205 "Via",
206 "Warning":
207 return true
208 }
209 return false
210}