mirror of
				https://gitea.com/Lydanne/buildx.git
				synced 2025-11-04 10:03:42 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			725 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			725 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
/*
 | 
						|
Copyright 2016 The Kubernetes 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 net
 | 
						|
 | 
						|
import (
 | 
						|
	"bufio"
 | 
						|
	"bytes"
 | 
						|
	"context"
 | 
						|
	"crypto/tls"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"io"
 | 
						|
	"mime"
 | 
						|
	"net"
 | 
						|
	"net/http"
 | 
						|
	"net/url"
 | 
						|
	"os"
 | 
						|
	"path"
 | 
						|
	"regexp"
 | 
						|
	"strconv"
 | 
						|
	"strings"
 | 
						|
	"unicode"
 | 
						|
	"unicode/utf8"
 | 
						|
 | 
						|
	"golang.org/x/net/http2"
 | 
						|
	"k8s.io/klog/v2"
 | 
						|
)
 | 
						|
 | 
						|
// JoinPreservingTrailingSlash does a path.Join of the specified elements,
 | 
						|
// preserving any trailing slash on the last non-empty segment
 | 
						|
func JoinPreservingTrailingSlash(elem ...string) string {
 | 
						|
	// do the basic path join
 | 
						|
	result := path.Join(elem...)
 | 
						|
 | 
						|
	// find the last non-empty segment
 | 
						|
	for i := len(elem) - 1; i >= 0; i-- {
 | 
						|
		if len(elem[i]) > 0 {
 | 
						|
			// if the last segment ended in a slash, ensure our result does as well
 | 
						|
			if strings.HasSuffix(elem[i], "/") && !strings.HasSuffix(result, "/") {
 | 
						|
				result += "/"
 | 
						|
			}
 | 
						|
			break
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return result
 | 
						|
}
 | 
						|
 | 
						|
// IsTimeout returns true if the given error is a network timeout error
 | 
						|
func IsTimeout(err error) bool {
 | 
						|
	var neterr net.Error
 | 
						|
	if errors.As(err, &neterr) {
 | 
						|
		return neterr != nil && neterr.Timeout()
 | 
						|
	}
 | 
						|
	return false
 | 
						|
}
 | 
						|
 | 
						|
// IsProbableEOF returns true if the given error resembles a connection termination
 | 
						|
// scenario that would justify assuming that the watch is empty.
 | 
						|
// These errors are what the Go http stack returns back to us which are general
 | 
						|
// connection closure errors (strongly correlated) and callers that need to
 | 
						|
// differentiate probable errors in connection behavior between normal "this is
 | 
						|
// disconnected" should use the method.
 | 
						|
func IsProbableEOF(err error) bool {
 | 
						|
	if err == nil {
 | 
						|
		return false
 | 
						|
	}
 | 
						|
	var uerr *url.Error
 | 
						|
	if errors.As(err, &uerr) {
 | 
						|
		err = uerr.Err
 | 
						|
	}
 | 
						|
	msg := err.Error()
 | 
						|
	switch {
 | 
						|
	case err == io.EOF:
 | 
						|
		return true
 | 
						|
	case err == io.ErrUnexpectedEOF:
 | 
						|
		return true
 | 
						|
	case msg == "http: can't write HTTP request on broken connection":
 | 
						|
		return true
 | 
						|
	case strings.Contains(msg, "http2: server sent GOAWAY and closed the connection"):
 | 
						|
		return true
 | 
						|
	case strings.Contains(msg, "connection reset by peer"):
 | 
						|
		return true
 | 
						|
	case strings.Contains(strings.ToLower(msg), "use of closed network connection"):
 | 
						|
		return true
 | 
						|
	}
 | 
						|
	return false
 | 
						|
}
 | 
						|
 | 
						|
var defaultTransport = http.DefaultTransport.(*http.Transport)
 | 
						|
 | 
						|
// SetOldTransportDefaults applies the defaults from http.DefaultTransport
 | 
						|
// for the Proxy, Dial, and TLSHandshakeTimeout fields if unset
 | 
						|
func SetOldTransportDefaults(t *http.Transport) *http.Transport {
 | 
						|
	if t.Proxy == nil || isDefault(t.Proxy) {
 | 
						|
		// http.ProxyFromEnvironment doesn't respect CIDRs and that makes it impossible to exclude things like pod and service IPs from proxy settings
 | 
						|
		// ProxierWithNoProxyCIDR allows CIDR rules in NO_PROXY
 | 
						|
		t.Proxy = NewProxierWithNoProxyCIDR(http.ProxyFromEnvironment)
 | 
						|
	}
 | 
						|
	// If no custom dialer is set, use the default context dialer
 | 
						|
	if t.DialContext == nil && t.Dial == nil {
 | 
						|
		t.DialContext = defaultTransport.DialContext
 | 
						|
	}
 | 
						|
	if t.TLSHandshakeTimeout == 0 {
 | 
						|
		t.TLSHandshakeTimeout = defaultTransport.TLSHandshakeTimeout
 | 
						|
	}
 | 
						|
	if t.IdleConnTimeout == 0 {
 | 
						|
		t.IdleConnTimeout = defaultTransport.IdleConnTimeout
 | 
						|
	}
 | 
						|
	return t
 | 
						|
}
 | 
						|
 | 
						|
// SetTransportDefaults applies the defaults from http.DefaultTransport
 | 
						|
// for the Proxy, Dial, and TLSHandshakeTimeout fields if unset
 | 
						|
func SetTransportDefaults(t *http.Transport) *http.Transport {
 | 
						|
	t = SetOldTransportDefaults(t)
 | 
						|
	// Allow clients to disable http2 if needed.
 | 
						|
	if s := os.Getenv("DISABLE_HTTP2"); len(s) > 0 {
 | 
						|
		klog.Infof("HTTP2 has been explicitly disabled")
 | 
						|
	} else if allowsHTTP2(t) {
 | 
						|
		if err := http2.ConfigureTransport(t); err != nil {
 | 
						|
			klog.Warningf("Transport failed http2 configuration: %v", err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return t
 | 
						|
}
 | 
						|
 | 
						|
func allowsHTTP2(t *http.Transport) bool {
 | 
						|
	if t.TLSClientConfig == nil || len(t.TLSClientConfig.NextProtos) == 0 {
 | 
						|
		// the transport expressed no NextProto preference, allow
 | 
						|
		return true
 | 
						|
	}
 | 
						|
	for _, p := range t.TLSClientConfig.NextProtos {
 | 
						|
		if p == http2.NextProtoTLS {
 | 
						|
			// the transport explicitly allowed http/2
 | 
						|
			return true
 | 
						|
		}
 | 
						|
	}
 | 
						|
	// the transport explicitly set NextProtos and excluded http/2
 | 
						|
	return false
 | 
						|
}
 | 
						|
 | 
						|
type RoundTripperWrapper interface {
 | 
						|
	http.RoundTripper
 | 
						|
	WrappedRoundTripper() http.RoundTripper
 | 
						|
}
 | 
						|
 | 
						|
type DialFunc func(ctx context.Context, net, addr string) (net.Conn, error)
 | 
						|
 | 
						|
func DialerFor(transport http.RoundTripper) (DialFunc, error) {
 | 
						|
	if transport == nil {
 | 
						|
		return nil, nil
 | 
						|
	}
 | 
						|
 | 
						|
	switch transport := transport.(type) {
 | 
						|
	case *http.Transport:
 | 
						|
		// transport.DialContext takes precedence over transport.Dial
 | 
						|
		if transport.DialContext != nil {
 | 
						|
			return transport.DialContext, nil
 | 
						|
		}
 | 
						|
		// adapt transport.Dial to the DialWithContext signature
 | 
						|
		if transport.Dial != nil {
 | 
						|
			return func(ctx context.Context, net, addr string) (net.Conn, error) {
 | 
						|
				return transport.Dial(net, addr)
 | 
						|
			}, nil
 | 
						|
		}
 | 
						|
		// otherwise return nil
 | 
						|
		return nil, nil
 | 
						|
	case RoundTripperWrapper:
 | 
						|
		return DialerFor(transport.WrappedRoundTripper())
 | 
						|
	default:
 | 
						|
		return nil, fmt.Errorf("unknown transport type: %T", transport)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
type TLSClientConfigHolder interface {
 | 
						|
	TLSClientConfig() *tls.Config
 | 
						|
}
 | 
						|
 | 
						|
func TLSClientConfig(transport http.RoundTripper) (*tls.Config, error) {
 | 
						|
	if transport == nil {
 | 
						|
		return nil, nil
 | 
						|
	}
 | 
						|
 | 
						|
	switch transport := transport.(type) {
 | 
						|
	case *http.Transport:
 | 
						|
		return transport.TLSClientConfig, nil
 | 
						|
	case TLSClientConfigHolder:
 | 
						|
		return transport.TLSClientConfig(), nil
 | 
						|
	case RoundTripperWrapper:
 | 
						|
		return TLSClientConfig(transport.WrappedRoundTripper())
 | 
						|
	default:
 | 
						|
		return nil, fmt.Errorf("unknown transport type: %T", transport)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func FormatURL(scheme string, host string, port int, path string) *url.URL {
 | 
						|
	return &url.URL{
 | 
						|
		Scheme: scheme,
 | 
						|
		Host:   net.JoinHostPort(host, strconv.Itoa(port)),
 | 
						|
		Path:   path,
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func GetHTTPClient(req *http.Request) string {
 | 
						|
	if ua := req.UserAgent(); len(ua) != 0 {
 | 
						|
		return ua
 | 
						|
	}
 | 
						|
	return "unknown"
 | 
						|
}
 | 
						|
 | 
						|
// SourceIPs splits the comma separated X-Forwarded-For header and joins it with
 | 
						|
// the X-Real-Ip header and/or req.RemoteAddr, ignoring invalid IPs.
 | 
						|
// The X-Real-Ip is omitted if it's already present in the X-Forwarded-For chain.
 | 
						|
// The req.RemoteAddr is always the last IP in the returned list.
 | 
						|
// It returns nil if all of these are empty or invalid.
 | 
						|
func SourceIPs(req *http.Request) []net.IP {
 | 
						|
	var srcIPs []net.IP
 | 
						|
 | 
						|
	hdr := req.Header
 | 
						|
	// First check the X-Forwarded-For header for requests via proxy.
 | 
						|
	hdrForwardedFor := hdr.Get("X-Forwarded-For")
 | 
						|
	if hdrForwardedFor != "" {
 | 
						|
		// X-Forwarded-For can be a csv of IPs in case of multiple proxies.
 | 
						|
		// Use the first valid one.
 | 
						|
		parts := strings.Split(hdrForwardedFor, ",")
 | 
						|
		for _, part := range parts {
 | 
						|
			ip := net.ParseIP(strings.TrimSpace(part))
 | 
						|
			if ip != nil {
 | 
						|
				srcIPs = append(srcIPs, ip)
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Try the X-Real-Ip header.
 | 
						|
	hdrRealIp := hdr.Get("X-Real-Ip")
 | 
						|
	if hdrRealIp != "" {
 | 
						|
		ip := net.ParseIP(hdrRealIp)
 | 
						|
		// Only append the X-Real-Ip if it's not already contained in the X-Forwarded-For chain.
 | 
						|
		if ip != nil && !containsIP(srcIPs, ip) {
 | 
						|
			srcIPs = append(srcIPs, ip)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Always include the request Remote Address as it cannot be easily spoofed.
 | 
						|
	var remoteIP net.IP
 | 
						|
	// Remote Address in Go's HTTP server is in the form host:port so we need to split that first.
 | 
						|
	host, _, err := net.SplitHostPort(req.RemoteAddr)
 | 
						|
	if err == nil {
 | 
						|
		remoteIP = net.ParseIP(host)
 | 
						|
	}
 | 
						|
	// Fallback if Remote Address was just IP.
 | 
						|
	if remoteIP == nil {
 | 
						|
		remoteIP = net.ParseIP(req.RemoteAddr)
 | 
						|
	}
 | 
						|
 | 
						|
	// Don't duplicate remote IP if it's already the last address in the chain.
 | 
						|
	if remoteIP != nil && (len(srcIPs) == 0 || !remoteIP.Equal(srcIPs[len(srcIPs)-1])) {
 | 
						|
		srcIPs = append(srcIPs, remoteIP)
 | 
						|
	}
 | 
						|
 | 
						|
	return srcIPs
 | 
						|
}
 | 
						|
 | 
						|
// Checks whether the given IP address is contained in the list of IPs.
 | 
						|
func containsIP(ips []net.IP, ip net.IP) bool {
 | 
						|
	for _, v := range ips {
 | 
						|
		if v.Equal(ip) {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return false
 | 
						|
}
 | 
						|
 | 
						|
// Extracts and returns the clients IP from the given request.
 | 
						|
// Looks at X-Forwarded-For header, X-Real-Ip header and request.RemoteAddr in that order.
 | 
						|
// Returns nil if none of them are set or is set to an invalid value.
 | 
						|
func GetClientIP(req *http.Request) net.IP {
 | 
						|
	ips := SourceIPs(req)
 | 
						|
	if len(ips) == 0 {
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	return ips[0]
 | 
						|
}
 | 
						|
 | 
						|
// Prepares the X-Forwarded-For header for another forwarding hop by appending the previous sender's
 | 
						|
// IP address to the X-Forwarded-For chain.
 | 
						|
func AppendForwardedForHeader(req *http.Request) {
 | 
						|
	// Copied from net/http/httputil/reverseproxy.go:
 | 
						|
	if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
 | 
						|
		// If we aren't the first proxy retain prior
 | 
						|
		// X-Forwarded-For information as a comma+space
 | 
						|
		// separated list and fold multiple headers into one.
 | 
						|
		if prior, ok := req.Header["X-Forwarded-For"]; ok {
 | 
						|
			clientIP = strings.Join(prior, ", ") + ", " + clientIP
 | 
						|
		}
 | 
						|
		req.Header.Set("X-Forwarded-For", clientIP)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
var defaultProxyFuncPointer = fmt.Sprintf("%p", http.ProxyFromEnvironment)
 | 
						|
 | 
						|
// isDefault checks to see if the transportProxierFunc is pointing to the default one
 | 
						|
func isDefault(transportProxier func(*http.Request) (*url.URL, error)) bool {
 | 
						|
	transportProxierPointer := fmt.Sprintf("%p", transportProxier)
 | 
						|
	return transportProxierPointer == defaultProxyFuncPointer
 | 
						|
}
 | 
						|
 | 
						|
// NewProxierWithNoProxyCIDR constructs a Proxier function that respects CIDRs in NO_PROXY and delegates if
 | 
						|
// no matching CIDRs are found
 | 
						|
func NewProxierWithNoProxyCIDR(delegate func(req *http.Request) (*url.URL, error)) func(req *http.Request) (*url.URL, error) {
 | 
						|
	// we wrap the default method, so we only need to perform our check if the NO_PROXY (or no_proxy) envvar has a CIDR in it
 | 
						|
	noProxyEnv := os.Getenv("NO_PROXY")
 | 
						|
	if noProxyEnv == "" {
 | 
						|
		noProxyEnv = os.Getenv("no_proxy")
 | 
						|
	}
 | 
						|
	noProxyRules := strings.Split(noProxyEnv, ",")
 | 
						|
 | 
						|
	cidrs := []*net.IPNet{}
 | 
						|
	for _, noProxyRule := range noProxyRules {
 | 
						|
		_, cidr, _ := net.ParseCIDR(noProxyRule)
 | 
						|
		if cidr != nil {
 | 
						|
			cidrs = append(cidrs, cidr)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if len(cidrs) == 0 {
 | 
						|
		return delegate
 | 
						|
	}
 | 
						|
 | 
						|
	return func(req *http.Request) (*url.URL, error) {
 | 
						|
		ip := net.ParseIP(req.URL.Hostname())
 | 
						|
		if ip == nil {
 | 
						|
			return delegate(req)
 | 
						|
		}
 | 
						|
 | 
						|
		for _, cidr := range cidrs {
 | 
						|
			if cidr.Contains(ip) {
 | 
						|
				return nil, nil
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		return delegate(req)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// DialerFunc implements Dialer for the provided function.
 | 
						|
type DialerFunc func(req *http.Request) (net.Conn, error)
 | 
						|
 | 
						|
func (fn DialerFunc) Dial(req *http.Request) (net.Conn, error) {
 | 
						|
	return fn(req)
 | 
						|
}
 | 
						|
 | 
						|
// Dialer dials a host and writes a request to it.
 | 
						|
type Dialer interface {
 | 
						|
	// Dial connects to the host specified by req's URL, writes the request to the connection, and
 | 
						|
	// returns the opened net.Conn.
 | 
						|
	Dial(req *http.Request) (net.Conn, error)
 | 
						|
}
 | 
						|
 | 
						|
// ConnectWithRedirects uses dialer to send req, following up to 10 redirects (relative to
 | 
						|
// originalLocation). It returns the opened net.Conn and the raw response bytes.
 | 
						|
// If requireSameHostRedirects is true, only redirects to the same host are permitted.
 | 
						|
func ConnectWithRedirects(originalMethod string, originalLocation *url.URL, header http.Header, originalBody io.Reader, dialer Dialer, requireSameHostRedirects bool) (net.Conn, []byte, error) {
 | 
						|
	const (
 | 
						|
		maxRedirects    = 9     // Fail on the 10th redirect
 | 
						|
		maxResponseSize = 16384 // play it safe to allow the potential for lots of / large headers
 | 
						|
	)
 | 
						|
 | 
						|
	var (
 | 
						|
		location         = originalLocation
 | 
						|
		method           = originalMethod
 | 
						|
		intermediateConn net.Conn
 | 
						|
		rawResponse      = bytes.NewBuffer(make([]byte, 0, 256))
 | 
						|
		body             = originalBody
 | 
						|
	)
 | 
						|
 | 
						|
	defer func() {
 | 
						|
		if intermediateConn != nil {
 | 
						|
			intermediateConn.Close()
 | 
						|
		}
 | 
						|
	}()
 | 
						|
 | 
						|
redirectLoop:
 | 
						|
	for redirects := 0; ; redirects++ {
 | 
						|
		if redirects > maxRedirects {
 | 
						|
			return nil, nil, fmt.Errorf("too many redirects (%d)", redirects)
 | 
						|
		}
 | 
						|
 | 
						|
		req, err := http.NewRequest(method, location.String(), body)
 | 
						|
		if err != nil {
 | 
						|
			return nil, nil, err
 | 
						|
		}
 | 
						|
 | 
						|
		req.Header = header
 | 
						|
 | 
						|
		intermediateConn, err = dialer.Dial(req)
 | 
						|
		if err != nil {
 | 
						|
			return nil, nil, err
 | 
						|
		}
 | 
						|
 | 
						|
		// Peek at the backend response.
 | 
						|
		rawResponse.Reset()
 | 
						|
		respReader := bufio.NewReader(io.TeeReader(
 | 
						|
			io.LimitReader(intermediateConn, maxResponseSize), // Don't read more than maxResponseSize bytes.
 | 
						|
			rawResponse)) // Save the raw response.
 | 
						|
		resp, err := http.ReadResponse(respReader, nil)
 | 
						|
		if err != nil {
 | 
						|
			// Unable to read the backend response; let the client handle it.
 | 
						|
			klog.Warningf("Error reading backend response: %v", err)
 | 
						|
			break redirectLoop
 | 
						|
		}
 | 
						|
 | 
						|
		switch resp.StatusCode {
 | 
						|
		case http.StatusFound:
 | 
						|
			// Redirect, continue.
 | 
						|
		default:
 | 
						|
			// Don't redirect.
 | 
						|
			break redirectLoop
 | 
						|
		}
 | 
						|
 | 
						|
		// Redirected requests switch to "GET" according to the HTTP spec:
 | 
						|
		// https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3
 | 
						|
		method = "GET"
 | 
						|
		// don't send a body when following redirects
 | 
						|
		body = nil
 | 
						|
 | 
						|
		resp.Body.Close() // not used
 | 
						|
 | 
						|
		// Prepare to follow the redirect.
 | 
						|
		redirectStr := resp.Header.Get("Location")
 | 
						|
		if redirectStr == "" {
 | 
						|
			return nil, nil, fmt.Errorf("%d response missing Location header", resp.StatusCode)
 | 
						|
		}
 | 
						|
		// We have to parse relative to the current location, NOT originalLocation. For example,
 | 
						|
		// if we request http://foo.com/a and get back "http://bar.com/b", the result should be
 | 
						|
		// http://bar.com/b. If we then make that request and get back a redirect to "/c", the result
 | 
						|
		// should be http://bar.com/c, not http://foo.com/c.
 | 
						|
		location, err = location.Parse(redirectStr)
 | 
						|
		if err != nil {
 | 
						|
			return nil, nil, fmt.Errorf("malformed Location header: %v", err)
 | 
						|
		}
 | 
						|
 | 
						|
		// Only follow redirects to the same host. Otherwise, propagate the redirect response back.
 | 
						|
		if requireSameHostRedirects && location.Hostname() != originalLocation.Hostname() {
 | 
						|
			return nil, nil, fmt.Errorf("hostname mismatch: expected %s, found %s", originalLocation.Hostname(), location.Hostname())
 | 
						|
		}
 | 
						|
 | 
						|
		// Reset the connection.
 | 
						|
		intermediateConn.Close()
 | 
						|
		intermediateConn = nil
 | 
						|
	}
 | 
						|
 | 
						|
	connToReturn := intermediateConn
 | 
						|
	intermediateConn = nil // Don't close the connection when we return it.
 | 
						|
	return connToReturn, rawResponse.Bytes(), nil
 | 
						|
}
 | 
						|
 | 
						|
// CloneRequest creates a shallow copy of the request along with a deep copy of the Headers.
 | 
						|
func CloneRequest(req *http.Request) *http.Request {
 | 
						|
	r := new(http.Request)
 | 
						|
 | 
						|
	// shallow clone
 | 
						|
	*r = *req
 | 
						|
 | 
						|
	// deep copy headers
 | 
						|
	r.Header = CloneHeader(req.Header)
 | 
						|
 | 
						|
	return r
 | 
						|
}
 | 
						|
 | 
						|
// CloneHeader creates a deep copy of an http.Header.
 | 
						|
func CloneHeader(in http.Header) http.Header {
 | 
						|
	out := make(http.Header, len(in))
 | 
						|
	for key, values := range in {
 | 
						|
		newValues := make([]string, len(values))
 | 
						|
		copy(newValues, values)
 | 
						|
		out[key] = newValues
 | 
						|
	}
 | 
						|
	return out
 | 
						|
}
 | 
						|
 | 
						|
// WarningHeader contains a single RFC2616 14.46 warnings header
 | 
						|
type WarningHeader struct {
 | 
						|
	// Codeindicates the type of warning. 299 is a miscellaneous persistent warning
 | 
						|
	Code int
 | 
						|
	// Agent contains the name or pseudonym of the server adding the Warning header.
 | 
						|
	// A single "-" is recommended when agent is unknown.
 | 
						|
	Agent string
 | 
						|
	// Warning text
 | 
						|
	Text string
 | 
						|
}
 | 
						|
 | 
						|
// ParseWarningHeaders extract RFC2616 14.46 warnings headers from the specified set of header values.
 | 
						|
// Multiple comma-separated warnings per header are supported.
 | 
						|
// If errors are encountered on a header, the remainder of that header are skipped and subsequent headers are parsed.
 | 
						|
// Returns successfully parsed warnings and any errors encountered.
 | 
						|
func ParseWarningHeaders(headers []string) ([]WarningHeader, []error) {
 | 
						|
	var (
 | 
						|
		results []WarningHeader
 | 
						|
		errs    []error
 | 
						|
	)
 | 
						|
	for _, header := range headers {
 | 
						|
		for len(header) > 0 {
 | 
						|
			result, remainder, err := ParseWarningHeader(header)
 | 
						|
			if err != nil {
 | 
						|
				errs = append(errs, err)
 | 
						|
				break
 | 
						|
			}
 | 
						|
			results = append(results, result)
 | 
						|
			header = remainder
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return results, errs
 | 
						|
}
 | 
						|
 | 
						|
var (
 | 
						|
	codeMatcher = regexp.MustCompile(`^[0-9]{3}$`)
 | 
						|
	wordDecoder = &mime.WordDecoder{}
 | 
						|
)
 | 
						|
 | 
						|
// ParseWarningHeader extracts one RFC2616 14.46 warning from the specified header,
 | 
						|
// returning an error if the header does not contain a correctly formatted warning.
 | 
						|
// Any remaining content in the header is returned.
 | 
						|
func ParseWarningHeader(header string) (result WarningHeader, remainder string, err error) {
 | 
						|
	// https://tools.ietf.org/html/rfc2616#section-14.46
 | 
						|
	//   updated by
 | 
						|
	// https://tools.ietf.org/html/rfc7234#section-5.5
 | 
						|
	//   https://tools.ietf.org/html/rfc7234#appendix-A
 | 
						|
	//     Some requirements regarding production and processing of the Warning
 | 
						|
	//     header fields have been relaxed, as it is not widely implemented.
 | 
						|
	//     Furthermore, the Warning header field no longer uses RFC 2047
 | 
						|
	//     encoding, nor does it allow multiple languages, as these aspects were
 | 
						|
	//     not implemented.
 | 
						|
	//
 | 
						|
	// Format is one of:
 | 
						|
	// warn-code warn-agent "warn-text"
 | 
						|
	// warn-code warn-agent "warn-text" "warn-date"
 | 
						|
	//
 | 
						|
	// warn-code is a three digit number
 | 
						|
	// warn-agent is unquoted and contains no spaces
 | 
						|
	// warn-text is quoted with backslash escaping (RFC2047-encoded according to RFC2616, not encoded according to RFC7234)
 | 
						|
	// warn-date is optional, quoted, and in HTTP-date format (no embedded or escaped quotes)
 | 
						|
	//
 | 
						|
	// additional warnings can optionally be included in the same header by comma-separating them:
 | 
						|
	// warn-code warn-agent "warn-text" "warn-date"[, warn-code warn-agent "warn-text" "warn-date", ...]
 | 
						|
 | 
						|
	// tolerate leading whitespace
 | 
						|
	header = strings.TrimSpace(header)
 | 
						|
 | 
						|
	parts := strings.SplitN(header, " ", 3)
 | 
						|
	if len(parts) != 3 {
 | 
						|
		return WarningHeader{}, "", errors.New("invalid warning header: fewer than 3 segments")
 | 
						|
	}
 | 
						|
	code, agent, textDateRemainder := parts[0], parts[1], parts[2]
 | 
						|
 | 
						|
	// verify code format
 | 
						|
	if !codeMatcher.Match([]byte(code)) {
 | 
						|
		return WarningHeader{}, "", errors.New("invalid warning header: code segment is not 3 digits between 100-299")
 | 
						|
	}
 | 
						|
	codeInt, _ := strconv.ParseInt(code, 10, 64)
 | 
						|
 | 
						|
	// verify agent presence
 | 
						|
	if len(agent) == 0 {
 | 
						|
		return WarningHeader{}, "", errors.New("invalid warning header: empty agent segment")
 | 
						|
	}
 | 
						|
	if !utf8.ValidString(agent) || hasAnyRunes(agent, unicode.IsControl) {
 | 
						|
		return WarningHeader{}, "", errors.New("invalid warning header: invalid agent")
 | 
						|
	}
 | 
						|
 | 
						|
	// verify textDateRemainder presence
 | 
						|
	if len(textDateRemainder) == 0 {
 | 
						|
		return WarningHeader{}, "", errors.New("invalid warning header: empty text segment")
 | 
						|
	}
 | 
						|
 | 
						|
	// extract text
 | 
						|
	text, dateAndRemainder, err := parseQuotedString(textDateRemainder)
 | 
						|
	if err != nil {
 | 
						|
		return WarningHeader{}, "", fmt.Errorf("invalid warning header: %v", err)
 | 
						|
	}
 | 
						|
	// tolerate RFC2047-encoded text from warnings produced according to RFC2616
 | 
						|
	if decodedText, err := wordDecoder.DecodeHeader(text); err == nil {
 | 
						|
		text = decodedText
 | 
						|
	}
 | 
						|
	if !utf8.ValidString(text) || hasAnyRunes(text, unicode.IsControl) {
 | 
						|
		return WarningHeader{}, "", errors.New("invalid warning header: invalid text")
 | 
						|
	}
 | 
						|
	result = WarningHeader{Code: int(codeInt), Agent: agent, Text: text}
 | 
						|
 | 
						|
	if len(dateAndRemainder) > 0 {
 | 
						|
		if dateAndRemainder[0] == '"' {
 | 
						|
			// consume date
 | 
						|
			foundEndQuote := false
 | 
						|
			for i := 1; i < len(dateAndRemainder); i++ {
 | 
						|
				if dateAndRemainder[i] == '"' {
 | 
						|
					foundEndQuote = true
 | 
						|
					remainder = strings.TrimSpace(dateAndRemainder[i+1:])
 | 
						|
					break
 | 
						|
				}
 | 
						|
			}
 | 
						|
			if !foundEndQuote {
 | 
						|
				return WarningHeader{}, "", errors.New("invalid warning header: unterminated date segment")
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			remainder = dateAndRemainder
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if len(remainder) > 0 {
 | 
						|
		if remainder[0] == ',' {
 | 
						|
			// consume comma if present
 | 
						|
			remainder = strings.TrimSpace(remainder[1:])
 | 
						|
		} else {
 | 
						|
			return WarningHeader{}, "", errors.New("invalid warning header: unexpected token after warn-date")
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return result, remainder, nil
 | 
						|
}
 | 
						|
 | 
						|
func parseQuotedString(quotedString string) (string, string, error) {
 | 
						|
	if len(quotedString) == 0 {
 | 
						|
		return "", "", errors.New("invalid quoted string: 0-length")
 | 
						|
	}
 | 
						|
 | 
						|
	if quotedString[0] != '"' {
 | 
						|
		return "", "", errors.New("invalid quoted string: missing initial quote")
 | 
						|
	}
 | 
						|
 | 
						|
	quotedString = quotedString[1:]
 | 
						|
	var remainder string
 | 
						|
	escaping := false
 | 
						|
	closedQuote := false
 | 
						|
	result := &bytes.Buffer{}
 | 
						|
loop:
 | 
						|
	for i := 0; i < len(quotedString); i++ {
 | 
						|
		b := quotedString[i]
 | 
						|
		switch b {
 | 
						|
		case '"':
 | 
						|
			if escaping {
 | 
						|
				result.WriteByte(b)
 | 
						|
				escaping = false
 | 
						|
			} else {
 | 
						|
				closedQuote = true
 | 
						|
				remainder = strings.TrimSpace(quotedString[i+1:])
 | 
						|
				break loop
 | 
						|
			}
 | 
						|
		case '\\':
 | 
						|
			if escaping {
 | 
						|
				result.WriteByte(b)
 | 
						|
				escaping = false
 | 
						|
			} else {
 | 
						|
				escaping = true
 | 
						|
			}
 | 
						|
		default:
 | 
						|
			result.WriteByte(b)
 | 
						|
			escaping = false
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if !closedQuote {
 | 
						|
		return "", "", errors.New("invalid quoted string: missing closing quote")
 | 
						|
	}
 | 
						|
	return result.String(), remainder, nil
 | 
						|
}
 | 
						|
 | 
						|
func NewWarningHeader(code int, agent, text string) (string, error) {
 | 
						|
	if code < 0 || code > 999 {
 | 
						|
		return "", errors.New("code must be between 0 and 999")
 | 
						|
	}
 | 
						|
	if len(agent) == 0 {
 | 
						|
		agent = "-"
 | 
						|
	} else if !utf8.ValidString(agent) || strings.ContainsAny(agent, `\"`) || hasAnyRunes(agent, unicode.IsSpace, unicode.IsControl) {
 | 
						|
		return "", errors.New("agent must be valid UTF-8 and must not contain spaces, quotes, backslashes, or control characters")
 | 
						|
	}
 | 
						|
	if !utf8.ValidString(text) || hasAnyRunes(text, unicode.IsControl) {
 | 
						|
		return "", errors.New("text must be valid UTF-8 and must not contain control characters")
 | 
						|
	}
 | 
						|
	return fmt.Sprintf("%03d %s %s", code, agent, makeQuotedString(text)), nil
 | 
						|
}
 | 
						|
 | 
						|
func hasAnyRunes(s string, runeCheckers ...func(rune) bool) bool {
 | 
						|
	for _, r := range s {
 | 
						|
		for _, checker := range runeCheckers {
 | 
						|
			if checker(r) {
 | 
						|
				return true
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return false
 | 
						|
}
 | 
						|
 | 
						|
func makeQuotedString(s string) string {
 | 
						|
	result := &bytes.Buffer{}
 | 
						|
	// opening quote
 | 
						|
	result.WriteRune('"')
 | 
						|
	for _, c := range s {
 | 
						|
		switch c {
 | 
						|
		case '"', '\\':
 | 
						|
			// escape " and \
 | 
						|
			result.WriteRune('\\')
 | 
						|
			result.WriteRune(c)
 | 
						|
		default:
 | 
						|
			// write everything else as-is
 | 
						|
			result.WriteRune(c)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	// closing quote
 | 
						|
	result.WriteRune('"')
 | 
						|
	return result.String()
 | 
						|
}
 |