github.com/Psiphon-Labs/psiphon-tunnel-core@v2.0.28+incompatible/psiphon/common/quic/gquic-go/h2quic/request_writer.go (about)

     1  package h2quic
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"net/http"
     7  	"strconv"
     8  	"strings"
     9  	"sync"
    10  
    11  	"golang.org/x/net/http/httpguts"
    12  	"golang.org/x/net/http2"
    13  	"golang.org/x/net/http2/hpack"
    14  
    15  	quic "github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/quic/gquic-go"
    16  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/quic/gquic-go/internal/protocol"
    17  	"github.com/Psiphon-Labs/psiphon-tunnel-core/psiphon/common/quic/gquic-go/internal/utils"
    18  )
    19  
    20  type requestWriter struct {
    21  	mutex        sync.Mutex
    22  	headerStream quic.Stream
    23  
    24  	henc *hpack.Encoder
    25  	hbuf bytes.Buffer // HPACK encoder writes into this
    26  
    27  	logger utils.Logger
    28  }
    29  
    30  const defaultUserAgent = "quic-go"
    31  
    32  func newRequestWriter(headerStream quic.Stream, logger utils.Logger) *requestWriter {
    33  	rw := &requestWriter{
    34  		headerStream: headerStream,
    35  		logger:       logger,
    36  	}
    37  	rw.henc = hpack.NewEncoder(&rw.hbuf)
    38  	return rw
    39  }
    40  
    41  func (w *requestWriter) WriteRequest(req *http.Request, dataStreamID protocol.StreamID, endStream, requestGzip bool) error {
    42  	// TODO: add support for trailers
    43  	// TODO: add support for gzip compression
    44  	// TODO: write continuation frames, if the header frame is too long
    45  
    46  	w.mutex.Lock()
    47  	defer w.mutex.Unlock()
    48  
    49  	w.encodeHeaders(req, requestGzip, "", actualContentLength(req))
    50  	h2framer := http2.NewFramer(w.headerStream, nil)
    51  	return h2framer.WriteHeaders(http2.HeadersFrameParam{
    52  		StreamID:      uint32(dataStreamID),
    53  		EndHeaders:    true,
    54  		EndStream:     endStream,
    55  		BlockFragment: w.hbuf.Bytes(),
    56  		Priority:      http2.PriorityParam{Weight: 0xff},
    57  	})
    58  }
    59  
    60  // the rest of this files is copied from http2.Transport
    61  func (w *requestWriter) encodeHeaders(req *http.Request, addGzipHeader bool, trailers string, contentLength int64) ([]byte, error) {
    62  	w.hbuf.Reset()
    63  
    64  	host := req.Host
    65  	if host == "" {
    66  		host = req.URL.Host
    67  	}
    68  	host, err := httpguts.PunycodeHostPort(host)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	var path string
    74  	if req.Method != "CONNECT" {
    75  		path = req.URL.RequestURI()
    76  		if !validPseudoPath(path) {
    77  			orig := path
    78  			path = strings.TrimPrefix(path, req.URL.Scheme+"://"+host)
    79  			if !validPseudoPath(path) {
    80  				if req.URL.Opaque != "" {
    81  					return nil, fmt.Errorf("invalid request :path %q from URL.Opaque = %q", orig, req.URL.Opaque)
    82  				}
    83  				return nil, fmt.Errorf("invalid request :path %q", orig)
    84  			}
    85  		}
    86  	}
    87  
    88  	// Check for any invalid headers and return an error before we
    89  	// potentially pollute our hpack state. (We want to be able to
    90  	// continue to reuse the hpack encoder for future requests)
    91  	for k, vv := range req.Header {
    92  		if !httpguts.ValidHeaderFieldName(k) {
    93  			return nil, fmt.Errorf("invalid HTTP header name %q", k)
    94  		}
    95  		for _, v := range vv {
    96  			if !httpguts.ValidHeaderFieldValue(v) {
    97  				return nil, fmt.Errorf("invalid HTTP header value %q for header %q", v, k)
    98  			}
    99  		}
   100  	}
   101  
   102  	// 8.1.2.3 Request Pseudo-Header Fields
   103  	// The :path pseudo-header field includes the path and query parts of the
   104  	// target URI (the path-absolute production and optionally a '?' character
   105  	// followed by the query production (see Sections 3.3 and 3.4 of
   106  	// [RFC3986]).
   107  	w.writeHeader(":authority", host)
   108  	w.writeHeader(":method", req.Method)
   109  	if req.Method != "CONNECT" {
   110  		w.writeHeader(":path", path)
   111  		w.writeHeader(":scheme", req.URL.Scheme)
   112  	}
   113  	if trailers != "" {
   114  		w.writeHeader("trailer", trailers)
   115  	}
   116  
   117  	var didUA bool
   118  	for k, vv := range req.Header {
   119  		lowKey := strings.ToLower(k)
   120  		switch lowKey {
   121  		case "host", "content-length":
   122  			// Host is :authority, already sent.
   123  			// Content-Length is automatic, set below.
   124  			continue
   125  		case "connection", "proxy-connection", "transfer-encoding", "upgrade", "keep-alive":
   126  			// Per 8.1.2.2 Connection-Specific Header
   127  			// Fields, don't send connection-specific
   128  			// fields. We have already checked if any
   129  			// are error-worthy so just ignore the rest.
   130  			continue
   131  		case "user-agent":
   132  			// Match Go's http1 behavior: at most one
   133  			// User-Agent. If set to nil or empty string,
   134  			// then omit it. Otherwise if not mentioned,
   135  			// include the default (below).
   136  			didUA = true
   137  			if len(vv) < 1 {
   138  				continue
   139  			}
   140  			vv = vv[:1]
   141  			if vv[0] == "" {
   142  				continue
   143  			}
   144  		}
   145  		for _, v := range vv {
   146  			w.writeHeader(lowKey, v)
   147  		}
   148  	}
   149  	if shouldSendReqContentLength(req.Method, contentLength) {
   150  		w.writeHeader("content-length", strconv.FormatInt(contentLength, 10))
   151  	}
   152  	if addGzipHeader {
   153  		w.writeHeader("accept-encoding", "gzip")
   154  	}
   155  	if !didUA {
   156  		w.writeHeader("user-agent", defaultUserAgent)
   157  	}
   158  	return w.hbuf.Bytes(), nil
   159  }
   160  
   161  func (w *requestWriter) writeHeader(name, value string) {
   162  	w.logger.Debugf("http2: Transport encoding header %q = %q", name, value)
   163  	w.henc.WriteField(hpack.HeaderField{Name: name, Value: value})
   164  }
   165  
   166  // shouldSendReqContentLength reports whether the http2.Transport should send
   167  // a "content-length" request header. This logic is basically a copy of the net/http
   168  // transferWriter.shouldSendContentLength.
   169  // The contentLength is the corrected contentLength (so 0 means actually 0, not unknown).
   170  // -1 means unknown.
   171  func shouldSendReqContentLength(method string, contentLength int64) bool {
   172  	if contentLength > 0 {
   173  		return true
   174  	}
   175  	if contentLength < 0 {
   176  		return false
   177  	}
   178  	// For zero bodies, whether we send a content-length depends on the method.
   179  	// It also kinda doesn't matter for http2 either way, with END_STREAM.
   180  	switch method {
   181  	case "POST", "PUT", "PATCH":
   182  		return true
   183  	default:
   184  		return false
   185  	}
   186  }
   187  
   188  func validPseudoPath(v string) bool {
   189  	return (len(v) > 0 && v[0] == '/' && (len(v) == 1 || v[1] != '/')) || v == "*"
   190  }
   191  
   192  // actualContentLength returns a sanitized version of
   193  // req.ContentLength, where 0 actually means zero (not unknown) and -1
   194  // means unknown.
   195  func actualContentLength(req *http.Request) int64 {
   196  	if req.Body == nil {
   197  		return 0
   198  	}
   199  	if req.ContentLength != 0 {
   200  		return req.ContentLength
   201  	}
   202  	return -1
   203  }