blob: c29f33df0156aa03a79afb1081ee35f25cdd205e [file] [log] [blame]
// Copyright 2016 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 (
"fmt"
"net/http"
"strconv"
"strings"
"unicode"
)
// This file implements "Accept" and "Accept-Encoding" HTTP header parser.
// Spec: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
// accept is a parsed "Accept" or "Accept-Encoding" HTTP header.
type accept []acceptItem
type acceptItem struct {
Value string // e.g. "application/json; encoding=utf-8"
QualityFactor float32
}
// parseAccept parses an "Accept" or "Accept-Encoding" HTTP header.
//
// See spec http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
//
// This implementation is slow. Does not support accept params.
func parseAccept(v string) (accept, error) {
if v == "" {
return nil, nil
}
var result accept
for _, t := range strings.Split(v, ",") {
t = strings.TrimSpace(t)
if t == "" {
continue
}
value, q := qParamSplit(t)
item := acceptItem{Value: value, QualityFactor: 1.0}
if q != "" {
qualityFactor, err := strconv.ParseFloat(q, 32)
if err != nil {
return nil, fmt.Errorf("q parameter: expected a floating-point number")
}
item.QualityFactor = float32(qualityFactor)
}
result = append(result, item)
}
return result, nil
}
// qParamSplit splits an acceptable item into value (e.g. media type) and
// the q parameter. Does not support accept extensions.
func qParamSplit(v string) (value string, q string) {
rest := v
for {
semicolon := strings.IndexRune(rest, ';')
if semicolon < 0 {
value = v
return
}
semicolonAbs := len(v) - len(rest) + semicolon // mark
rest = rest[semicolon:]
rest = rest[1:] // consume ;
rest = strings.TrimLeftFunc(rest, unicode.IsSpace)
if rest == "" || (rest[0] != 'q' && rest[0] != 'Q') {
continue
}
rest = rest[1:] // consume q
rest = strings.TrimLeftFunc(rest, unicode.IsSpace)
if rest == "" || rest[0] != '=' {
continue
}
rest = rest[1:] // consume =
rest = strings.TrimLeftFunc(rest, unicode.IsSpace)
if rest == "" {
continue
}
qValueStartAbs := len(v) - len(rest) // mark
semicolon2 := strings.IndexRune(rest, ';')
if semicolon2 >= 0 {
semicolon2Abs := len(v) - len(rest) + semicolon2
value = v[:semicolonAbs]
q = v[qValueStartAbs:semicolon2Abs]
} else {
value = v[:semicolonAbs]
q = v[qValueStartAbs:]
}
q = strings.TrimRightFunc(q, unicode.IsSpace)
return
}
}
// acceptFormat is a format specified in "Accept" header.
type acceptFormat struct {
Format Format
QualityFactor float32 // preference, range: [0.0, 0.1]
}
// acceptFormatSlice is sortable by quality factor (desc) and format.
type acceptFormatSlice []acceptFormat
func (s acceptFormatSlice) Len() int {
return len(s)
}
func (s acceptFormatSlice) Less(i, j int) bool {
a, b := s[i], s[j]
const epsilon = 0.000000001
// quality factor descending
if a.QualityFactor+epsilon > b.QualityFactor {
return true
}
if a.QualityFactor+epsilon < b.QualityFactor {
return false
}
// format ascending
return a.Format < b.Format
}
func (s acceptFormatSlice) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
// acceptsGZipResponse returns true if the server response body may be encoded
// with GZIP.
func acceptsGZipResponse(header http.Header) (bool, error) {
accept, err := parseAccept(header.Get("Accept-Encoding"))
if err != nil {
return false, err
}
for _, a := range accept {
if strings.EqualFold(a.Value, "gzip") {
return true, nil
}
}
return false, nil
}