mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-11-14 00:07:00 +01:00
272 lines
7.4 KiB
Go
272 lines
7.4 KiB
Go
// Response selection follows the MIT-licensed compression and negotiator packages.
|
|
// See LICENSE.compression and LICENSE.negotiator.
|
|
package server
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"compress/zlib"
|
|
"io"
|
|
"math"
|
|
"net/http"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/andybalholm/brotli"
|
|
"github.com/tdurieux/anonymous_github/backend/internal/filetype"
|
|
)
|
|
|
|
var compressionFilePath = regexp.MustCompile(`^/api/repo/.+/file/`)
|
|
var compressionNoTransform = regexp.MustCompile(`(?i)(?:^|,)\s*no-transform\s*(?:,|$)`)
|
|
var encodingToken = regexp.MustCompile(`^\s*([^\s;]+)\s*(?:;(.*))?$`)
|
|
var qualityPrefix = regexp.MustCompile(`^[+-]?(?:Infinity|(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?)`)
|
|
|
|
type encodingPreference struct {
|
|
name string
|
|
quality float64
|
|
specificity, order, index int
|
|
}
|
|
|
|
func compressionEncoding(header string) string {
|
|
if header == "" {
|
|
return ""
|
|
}
|
|
accepted := []encodingPreference{}
|
|
identity := false
|
|
minimum := float64(1)
|
|
fields := strings.Split(header, ",")
|
|
for i, field := range fields {
|
|
m := encodingToken.FindStringSubmatch(strings.TrimSpace(field))
|
|
if m == nil {
|
|
continue
|
|
}
|
|
q := float64(1)
|
|
for _, param := range strings.Split(m[2], ";") {
|
|
parts := strings.Split(strings.TrimSpace(param), "=")
|
|
if parts[0] != "q" {
|
|
continue
|
|
}
|
|
q = math.NaN()
|
|
if len(parts) > 1 {
|
|
prefix := qualityPrefix.FindString(strings.TrimSpace(parts[1]))
|
|
if prefix == "Infinity" || prefix == "+Infinity" {
|
|
q = math.Inf(1)
|
|
} else if prefix == "-Infinity" {
|
|
q = math.Inf(-1)
|
|
} else if prefix != "" {
|
|
q, _ = strconv.ParseFloat(prefix, 64)
|
|
}
|
|
}
|
|
break
|
|
}
|
|
name := strings.ToLower(m[1])
|
|
identity = identity || name == "identity" || name == "*"
|
|
if q != 0 && !math.IsNaN(q) {
|
|
minimum = math.Min(minimum, q)
|
|
}
|
|
accepted = append(accepted, encodingPreference{name: name, quality: q, order: i})
|
|
}
|
|
if !identity {
|
|
accepted = append(accepted, encodingPreference{name: "identity", quality: minimum, order: len(fields)})
|
|
}
|
|
choices := []encodingPreference{}
|
|
for index, name := range []string{"br", "gzip", "deflate", "identity"} {
|
|
priority := encodingPreference{name: name, order: -1, index: index}
|
|
for _, a := range accepted {
|
|
specificity := 0
|
|
if a.name == name {
|
|
specificity = 1
|
|
} else if a.name != "*" {
|
|
continue
|
|
}
|
|
diff := float64(priority.specificity - specificity)
|
|
if diff == 0 {
|
|
diff = priority.quality - a.quality
|
|
}
|
|
if diff == 0 || math.IsNaN(diff) {
|
|
diff = float64(priority.order - a.order)
|
|
}
|
|
if diff < 0 {
|
|
priority = encodingPreference{name: name, quality: a.quality, specificity: specificity, order: a.order, index: index}
|
|
}
|
|
}
|
|
if priority.quality > 0 {
|
|
choices = append(choices, priority)
|
|
}
|
|
}
|
|
sort.SliceStable(choices, func(i, j int) bool {
|
|
a, b := choices[i], choices[j]
|
|
if a.quality != b.quality {
|
|
return a.quality > b.quality
|
|
}
|
|
if a.index < 2 || b.index < 2 {
|
|
return a.index < b.index
|
|
}
|
|
if a.specificity != b.specificity {
|
|
return a.specificity > b.specificity
|
|
}
|
|
if a.order != b.order {
|
|
return a.order < b.order
|
|
}
|
|
return a.index < b.index
|
|
})
|
|
if len(choices) == 0 || choices[0].name == "identity" {
|
|
return ""
|
|
}
|
|
return choices[0].name
|
|
}
|
|
|
|
type compressionWriter struct {
|
|
http.ResponseWriter
|
|
request *http.Request
|
|
encoder io.WriteCloser
|
|
started bool
|
|
status int
|
|
failure error
|
|
}
|
|
|
|
func (w *compressionWriter) Unwrap() http.ResponseWriter { return w.ResponseWriter }
|
|
func (w *compressionWriter) WriteHeader(status int) {
|
|
if status >= 100 && status < 200 {
|
|
w.ResponseWriter.WriteHeader(status)
|
|
return
|
|
}
|
|
if w.started {
|
|
return
|
|
}
|
|
w.started = true
|
|
w.status = status
|
|
h := w.Header()
|
|
if !compressionFilePath.MatchString(w.request.URL.Path) && filetype.Compressible(h.Get("Content-Type")) && !compressionNoTransform.MatchString(h.Get("Cache-Control")) {
|
|
vary := strings.Join(h.Values("Vary"), ", ")
|
|
present := false
|
|
for _, field := range strings.Split(vary, ",") {
|
|
if strings.TrimSpace(field) == "*" || strings.EqualFold(strings.TrimSpace(field), "Accept-Encoding") {
|
|
present = true
|
|
}
|
|
}
|
|
if !present {
|
|
if vary != "" {
|
|
vary += ", "
|
|
}
|
|
h.Set("Vary", vary+"Accept-Encoding")
|
|
}
|
|
length, err := strconv.ParseInt(h.Get("Content-Length"), 10, 64)
|
|
encoding := h.Get("Content-Encoding")
|
|
if (err != nil || length >= 1024) && (encoding == "" || encoding == "identity") && w.request.Method != "HEAD" {
|
|
method := compressionEncoding(strings.Join(w.request.Header.Values("Accept-Encoding"), ", "))
|
|
if method != "" {
|
|
h.Set("Content-Encoding", method)
|
|
h.Del("Content-Length")
|
|
if status != 204 && status != 304 {
|
|
switch method {
|
|
case "gzip":
|
|
w.encoder = gzip.NewWriter(w.ResponseWriter)
|
|
case "deflate":
|
|
w.encoder = zlib.NewWriter(w.ResponseWriter)
|
|
case "br":
|
|
w.encoder = brotli.NewWriterLevel(w.ResponseWriter, 4)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Prevent net/http from inventing a compressed Content-Length after
|
|
// buffering a small stream. Node commits these responses as chunked.
|
|
if h.Get("Content-Length") == "" && status != 204 && status != 304 && w.request.Method != "HEAD" && w.request.ProtoMajor == 1 && w.request.ProtoMinor >= 1 {
|
|
h.Set("Transfer-Encoding", "chunked")
|
|
}
|
|
w.ResponseWriter.WriteHeader(status)
|
|
}
|
|
func (w *compressionWriter) Write(data []byte) (int, error) {
|
|
if err := w.request.Context().Err(); err != nil {
|
|
w.failure = err
|
|
return 0, err
|
|
}
|
|
if !w.started {
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
if w.failure != nil {
|
|
return 0, w.failure
|
|
}
|
|
if w.status == http.StatusNoContent || w.status == http.StatusNotModified {
|
|
return 0, http.ErrBodyNotAllowed
|
|
}
|
|
if w.encoder != nil {
|
|
n, err := w.encoder.Write(data)
|
|
w.failure = err
|
|
return n, err
|
|
}
|
|
n, err := w.ResponseWriter.Write(data)
|
|
w.failure = err
|
|
return n, err
|
|
}
|
|
func (w *compressionWriter) Flush() {
|
|
if err := w.request.Context().Err(); err != nil {
|
|
w.failure = err
|
|
return
|
|
}
|
|
if !w.started {
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
if encoder, ok := w.encoder.(interface{ Flush() error }); ok {
|
|
if err := encoder.Flush(); err != nil {
|
|
w.failure = err
|
|
return
|
|
}
|
|
}
|
|
if err := http.NewResponseController(w.ResponseWriter).Flush(); err != nil {
|
|
w.failure = err
|
|
}
|
|
}
|
|
func (w *compressionWriter) finish() error {
|
|
if err := w.request.Context().Err(); err != nil {
|
|
return err
|
|
}
|
|
if w.failure != nil {
|
|
return w.failure
|
|
}
|
|
if w.encoder != nil {
|
|
return w.encoder.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Redirect responses are complete small bodies, not unknown-length streams.
|
|
// Defer their status until http.Redirect supplies the encoded body length.
|
|
type redirectLengthWriter struct {
|
|
http.ResponseWriter
|
|
status int
|
|
committed, head bool
|
|
}
|
|
|
|
func (w *redirectLengthWriter) WriteHeader(status int) {
|
|
if !w.committed {
|
|
w.status = status
|
|
}
|
|
}
|
|
func (w *redirectLengthWriter) Write(data []byte) (int, error) {
|
|
if !w.committed {
|
|
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
|
|
w.ResponseWriter.WriteHeader(w.status)
|
|
w.committed = true
|
|
}
|
|
if w.head {
|
|
return len(data), nil
|
|
}
|
|
return w.ResponseWriter.Write(data)
|
|
}
|
|
func redirectKnownLength(w http.ResponseWriter, r *http.Request, target string, status int) {
|
|
deferred := &redirectLengthWriter{ResponseWriter: w, status: status, head: r.Method == http.MethodHead}
|
|
if deferred.head {
|
|
r = r.Clone(r.Context())
|
|
r.Method = http.MethodGet
|
|
}
|
|
http.Redirect(deferred, r, target, status)
|
|
if !deferred.committed {
|
|
deferred.Header().Set("Content-Length", "0")
|
|
deferred.ResponseWriter.WriteHeader(deferred.status)
|
|
}
|
|
}
|