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 }