blob: 25c2252501a94a76dc5a110134c4728562e54640 [file] [log] [blame]
// Copyright 2019 The LUCI 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 prpc
import (
"encoding/base64"
"net/http"
"strings"
"google.golang.org/grpc/metadata"
"go.chromium.org/luci/common/errors"
)
// Standard HTTP headers that are vital for the pRPC protocol and should not
// be messed with via prpc.SetHeader(...).
var vitalHTTPHeaders = map[string]struct{}{
"Accept": {}, // content type negotiation
"Accept-Encoding": {}, // content type negotiation
"Content-Encoding": {}, // request/response compression
"Content-Length": {}, // vital for HTTP
"Content-Type": {}, // based on negotiated content type
"Vary": {}, // part of CORS response
"X-Content-Type-Options": {}, // always set to "nosniff"
}
// isReservedMetadataKey returns true for disallowed metadata keys.
//
// Keys are given in HTTP header canonical format.
//
// Setting metadata with such keys may break the protocol.
// See also Client.prepareRequest.
func isReservedMetadataKey(k string) bool {
// These standard HTTP headers are vital for the pRPC protocol and should not
// be messed with.
if _, yes := vitalHTTPHeaders[k]; yes {
return true
}
// These headers are used internally by pRPC protocol and should not be
// messed with.
if strings.HasPrefix(k, "X-Prpc-") {
return true
}
// CORS policies should be set via server.AccessControl callback, not via
// individual RPCs.
if strings.HasPrefix(k, "Access-Control-") {
return true
}
return false
}
// metaIntoHeaders merges outgoing metadata into the given set of headers.
//
// Encodes metadata entries with keys that end with "-bin".
func metaIntoHeaders(md metadata.MD, h http.Header) error {
for k, vs := range md {
canon := http.CanonicalHeaderKey(k)
if isReservedMetadataKey(canon) {
return errors.Reason("using reserved metadata key %q", k).Err()
}
if !strings.HasSuffix(canon, "-Bin") {
h[canon] = append(h[canon], vs...)
} else {
for _, v := range vs {
h[canon] = append(h[canon], base64.StdEncoding.EncodeToString([]byte(v)))
}
}
}
return nil
}
// headerIntoMeta merges values of the given header key into the metadata.
//
// Decodes entries with keys that end with "-bin". See also readStatusDetails
// which also does this decoding specifically for "X-Prpc-Status-Details-Bin".
func headerIntoMeta(key string, values []string, md metadata.MD) error {
key = strings.ToLower(key)
if !strings.HasSuffix(key, "-bin") {
md[key] = append(md[key], values...)
return nil
}
for _, v := range values {
decoded, err := base64.StdEncoding.DecodeString(v)
if err != nil {
return err
}
md[key] = append(md[key], string(decoded))
}
return nil
}
// headersIntoMetadata returns a new metadata.MD constructed from given headers.
//
// All reserved headers are silently skipped. If there's nothing left, returns
// nil.
func headersIntoMetadata(h http.Header) (md metadata.MD, err error) {
for k, v := range h {
if isReservedMetadataKey(http.CanonicalHeaderKey(k)) {
continue
}
if md == nil {
md = make(metadata.MD, len(h))
}
if err := headerIntoMeta(k, v, md); err != nil {
return nil, errors.Annotate(err, "can't decode header %q", k).Err()
}
}
return
}