github.com/andybalholm/brotli@v1.0.6/http.go (about)

     1  package brotli
     2  
     3  import (
     4  	"compress/gzip"
     5  	"io"
     6  	"net/http"
     7  	"strings"
     8  )
     9  
    10  // HTTPCompressor chooses a compression method (brotli, gzip, or none) based on
    11  // the Accept-Encoding header, sets the Content-Encoding header, and returns a
    12  // WriteCloser that implements that compression. The Close method must be called
    13  // before the current HTTP handler returns.
    14  func HTTPCompressor(w http.ResponseWriter, r *http.Request) io.WriteCloser {
    15  	if w.Header().Get("Vary") == "" {
    16  		w.Header().Set("Vary", "Accept-Encoding")
    17  	}
    18  
    19  	encoding := negotiateContentEncoding(r, []string{"br", "gzip"})
    20  	switch encoding {
    21  	case "br":
    22  		w.Header().Set("Content-Encoding", "br")
    23  		return NewWriter(w)
    24  	case "gzip":
    25  		w.Header().Set("Content-Encoding", "gzip")
    26  		return gzip.NewWriter(w)
    27  	}
    28  	return nopCloser{w}
    29  }
    30  
    31  // negotiateContentEncoding returns the best offered content encoding for the
    32  // request's Accept-Encoding header. If two offers match with equal weight and
    33  // then the offer earlier in the list is preferred. If no offers are
    34  // acceptable, then "" is returned.
    35  func negotiateContentEncoding(r *http.Request, offers []string) string {
    36  	bestOffer := "identity"
    37  	bestQ := -1.0
    38  	specs := parseAccept(r.Header, "Accept-Encoding")
    39  	for _, offer := range offers {
    40  		for _, spec := range specs {
    41  			if spec.Q > bestQ &&
    42  				(spec.Value == "*" || spec.Value == offer) {
    43  				bestQ = spec.Q
    44  				bestOffer = offer
    45  			}
    46  		}
    47  	}
    48  	if bestQ == 0 {
    49  		bestOffer = ""
    50  	}
    51  	return bestOffer
    52  }
    53  
    54  // acceptSpec describes an Accept* header.
    55  type acceptSpec struct {
    56  	Value string
    57  	Q     float64
    58  }
    59  
    60  // parseAccept parses Accept* headers.
    61  func parseAccept(header http.Header, key string) (specs []acceptSpec) {
    62  loop:
    63  	for _, s := range header[key] {
    64  		for {
    65  			var spec acceptSpec
    66  			spec.Value, s = expectTokenSlash(s)
    67  			if spec.Value == "" {
    68  				continue loop
    69  			}
    70  			spec.Q = 1.0
    71  			s = skipSpace(s)
    72  			if strings.HasPrefix(s, ";") {
    73  				s = skipSpace(s[1:])
    74  				if !strings.HasPrefix(s, "q=") {
    75  					continue loop
    76  				}
    77  				spec.Q, s = expectQuality(s[2:])
    78  				if spec.Q < 0.0 {
    79  					continue loop
    80  				}
    81  			}
    82  			specs = append(specs, spec)
    83  			s = skipSpace(s)
    84  			if !strings.HasPrefix(s, ",") {
    85  				continue loop
    86  			}
    87  			s = skipSpace(s[1:])
    88  		}
    89  	}
    90  	return
    91  }
    92  
    93  func skipSpace(s string) (rest string) {
    94  	i := 0
    95  	for ; i < len(s); i++ {
    96  		if octetTypes[s[i]]&isSpace == 0 {
    97  			break
    98  		}
    99  	}
   100  	return s[i:]
   101  }
   102  
   103  func expectTokenSlash(s string) (token, rest string) {
   104  	i := 0
   105  	for ; i < len(s); i++ {
   106  		b := s[i]
   107  		if (octetTypes[b]&isToken == 0) && b != '/' {
   108  			break
   109  		}
   110  	}
   111  	return s[:i], s[i:]
   112  }
   113  
   114  func expectQuality(s string) (q float64, rest string) {
   115  	switch {
   116  	case len(s) == 0:
   117  		return -1, ""
   118  	case s[0] == '0':
   119  		q = 0
   120  	case s[0] == '1':
   121  		q = 1
   122  	default:
   123  		return -1, ""
   124  	}
   125  	s = s[1:]
   126  	if !strings.HasPrefix(s, ".") {
   127  		return q, s
   128  	}
   129  	s = s[1:]
   130  	i := 0
   131  	n := 0
   132  	d := 1
   133  	for ; i < len(s); i++ {
   134  		b := s[i]
   135  		if b < '0' || b > '9' {
   136  			break
   137  		}
   138  		n = n*10 + int(b) - '0'
   139  		d *= 10
   140  	}
   141  	return q + float64(n)/float64(d), s[i:]
   142  }
   143  
   144  // Octet types from RFC 2616.
   145  var octetTypes [256]octetType
   146  
   147  type octetType byte
   148  
   149  const (
   150  	isToken octetType = 1 << iota
   151  	isSpace
   152  )
   153  
   154  func init() {
   155  	// OCTET      = <any 8-bit sequence of data>
   156  	// CHAR       = <any US-ASCII character (octets 0 - 127)>
   157  	// CTL        = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
   158  	// CR         = <US-ASCII CR, carriage return (13)>
   159  	// LF         = <US-ASCII LF, linefeed (10)>
   160  	// SP         = <US-ASCII SP, space (32)>
   161  	// HT         = <US-ASCII HT, horizontal-tab (9)>
   162  	// <">        = <US-ASCII double-quote mark (34)>
   163  	// CRLF       = CR LF
   164  	// LWS        = [CRLF] 1*( SP | HT )
   165  	// TEXT       = <any OCTET except CTLs, but including LWS>
   166  	// separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
   167  	//              | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
   168  	// token      = 1*<any CHAR except CTLs or separators>
   169  	// qdtext     = <any TEXT except <">>
   170  
   171  	for c := 0; c < 256; c++ {
   172  		var t octetType
   173  		isCtl := c <= 31 || c == 127
   174  		isChar := 0 <= c && c <= 127
   175  		isSeparator := strings.ContainsRune(" \t\"(),/:;<=>?@[]\\{}", rune(c))
   176  		if strings.ContainsRune(" \t\r\n", rune(c)) {
   177  			t |= isSpace
   178  		}
   179  		if isChar && !isCtl && !isSeparator {
   180  			t |= isToken
   181  		}
   182  		octetTypes[c] = t
   183  	}
   184  }