github.com/apernet/quic-go@v0.43.1-0.20240515053213-5e9e635fd9f0/http3/request_writer.go (about) 1 package http3 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "io" 8 "net" 9 "net/http" 10 "strconv" 11 "strings" 12 "sync" 13 14 "golang.org/x/net/http/httpguts" 15 "golang.org/x/net/http2/hpack" 16 "golang.org/x/net/idna" 17 18 "github.com/quic-go/qpack" 19 "github.com/apernet/quic-go" 20 ) 21 22 const bodyCopyBufferSize = 8 * 1024 23 24 type requestWriter struct { 25 mutex sync.Mutex 26 encoder *qpack.Encoder 27 headerBuf *bytes.Buffer 28 } 29 30 func newRequestWriter() *requestWriter { 31 headerBuf := &bytes.Buffer{} 32 encoder := qpack.NewEncoder(headerBuf) 33 return &requestWriter{ 34 encoder: encoder, 35 headerBuf: headerBuf, 36 } 37 } 38 39 func (w *requestWriter) WriteRequestHeader(str quic.Stream, req *http.Request, gzip bool) error { 40 // TODO: figure out how to add support for trailers 41 buf := &bytes.Buffer{} 42 if err := w.writeHeaders(buf, req, gzip); err != nil { 43 return err 44 } 45 _, err := str.Write(buf.Bytes()) 46 return err 47 } 48 49 func (w *requestWriter) writeHeaders(wr io.Writer, req *http.Request, gzip bool) error { 50 w.mutex.Lock() 51 defer w.mutex.Unlock() 52 defer w.encoder.Close() 53 defer w.headerBuf.Reset() 54 55 if err := w.encodeHeaders(req, gzip, "", actualContentLength(req)); err != nil { 56 return err 57 } 58 59 b := make([]byte, 0, 128) 60 b = (&headersFrame{Length: uint64(w.headerBuf.Len())}).Append(b) 61 if _, err := wr.Write(b); err != nil { 62 return err 63 } 64 _, err := wr.Write(w.headerBuf.Bytes()) 65 return err 66 } 67 68 func isExtendedConnectRequest(req *http.Request) bool { 69 return req.Method == http.MethodConnect && req.Proto != "" && req.Proto != "HTTP/1.1" 70 } 71 72 // copied from net/transport.go 73 // Modified to support Extended CONNECT: 74 // Contrary to what the godoc for the http.Request says, 75 // we do respect the Proto field if the method is CONNECT. 76 func (w *requestWriter) encodeHeaders(req *http.Request, addGzipHeader bool, trailers string, contentLength int64) error { 77 host := req.Host 78 if host == "" { 79 host = req.URL.Host 80 } 81 host, err := httpguts.PunycodeHostPort(host) 82 if err != nil { 83 return err 84 } 85 if !httpguts.ValidHostHeader(host) { 86 return errors.New("http3: invalid Host header") 87 } 88 89 // http.NewRequest sets this field to HTTP/1.1 90 isExtendedConnect := isExtendedConnectRequest(req) 91 92 var path string 93 if req.Method != http.MethodConnect || isExtendedConnect { 94 path = req.URL.RequestURI() 95 if !validPseudoPath(path) { 96 orig := path 97 path = strings.TrimPrefix(path, req.URL.Scheme+"://"+host) 98 if !validPseudoPath(path) { 99 if req.URL.Opaque != "" { 100 return fmt.Errorf("invalid request :path %q from URL.Opaque = %q", orig, req.URL.Opaque) 101 } else { 102 return fmt.Errorf("invalid request :path %q", orig) 103 } 104 } 105 } 106 } 107 108 // Check for any invalid headers and return an error before we 109 // potentially pollute our hpack state. (We want to be able to 110 // continue to reuse the hpack encoder for future requests) 111 for k, vv := range req.Header { 112 if !httpguts.ValidHeaderFieldName(k) { 113 return fmt.Errorf("invalid HTTP header name %q", k) 114 } 115 for _, v := range vv { 116 if !httpguts.ValidHeaderFieldValue(v) { 117 return fmt.Errorf("invalid HTTP header value %q for header %q", v, k) 118 } 119 } 120 } 121 122 enumerateHeaders := func(f func(name, value string)) { 123 // 8.1.2.3 Request Pseudo-Header Fields 124 // The :path pseudo-header field includes the path and query parts of the 125 // target URI (the path-absolute production and optionally a '?' character 126 // followed by the query production (see Sections 3.3 and 3.4 of 127 // [RFC3986]). 128 f(":authority", host) 129 f(":method", req.Method) 130 if req.Method != http.MethodConnect || isExtendedConnect { 131 f(":path", path) 132 f(":scheme", req.URL.Scheme) 133 } 134 if isExtendedConnect { 135 f(":protocol", req.Proto) 136 } 137 if trailers != "" { 138 f("trailer", trailers) 139 } 140 141 var didUA bool 142 for k, vv := range req.Header { 143 if strings.EqualFold(k, "host") || strings.EqualFold(k, "content-length") { 144 // Host is :authority, already sent. 145 // Content-Length is automatic, set below. 146 continue 147 } else if strings.EqualFold(k, "connection") || strings.EqualFold(k, "proxy-connection") || 148 strings.EqualFold(k, "transfer-encoding") || strings.EqualFold(k, "upgrade") || 149 strings.EqualFold(k, "keep-alive") { 150 // Per 8.1.2.2 Connection-Specific Header 151 // Fields, don't send connection-specific 152 // fields. We have already checked if any 153 // are error-worthy so just ignore the rest. 154 continue 155 } else if strings.EqualFold(k, "user-agent") { 156 // Match Go's http1 behavior: at most one 157 // User-Agent. If set to nil or empty string, 158 // then omit it. Otherwise if not mentioned, 159 // include the default (below). 160 didUA = true 161 if len(vv) < 1 { 162 continue 163 } 164 vv = vv[:1] 165 if vv[0] == "" { 166 continue 167 } 168 169 } 170 171 for _, v := range vv { 172 f(k, v) 173 } 174 } 175 if shouldSendReqContentLength(req.Method, contentLength) { 176 f("content-length", strconv.FormatInt(contentLength, 10)) 177 } 178 if addGzipHeader { 179 f("accept-encoding", "gzip") 180 } 181 if !didUA { 182 f("user-agent", defaultUserAgent) 183 } 184 } 185 186 // Do a first pass over the headers counting bytes to ensure 187 // we don't exceed cc.peerMaxHeaderListSize. This is done as a 188 // separate pass before encoding the headers to prevent 189 // modifying the hpack state. 190 hlSize := uint64(0) 191 enumerateHeaders(func(name, value string) { 192 hf := hpack.HeaderField{Name: name, Value: value} 193 hlSize += uint64(hf.Size()) 194 }) 195 196 // TODO: check maximum header list size 197 // if hlSize > cc.peerMaxHeaderListSize { 198 // return errRequestHeaderListSize 199 // } 200 201 // trace := httptrace.ContextClientTrace(req.Context()) 202 // traceHeaders := traceHasWroteHeaderField(trace) 203 204 // Header list size is ok. Write the headers. 205 enumerateHeaders(func(name, value string) { 206 name = strings.ToLower(name) 207 w.encoder.WriteField(qpack.HeaderField{Name: name, Value: value}) 208 // if traceHeaders { 209 // traceWroteHeaderField(trace, name, value) 210 // } 211 }) 212 213 return nil 214 } 215 216 // authorityAddr returns a given authority (a host/IP, or host:port / ip:port) 217 // and returns a host:port. The port 443 is added if needed. 218 func authorityAddr(authority string) (addr string) { 219 host, port, err := net.SplitHostPort(authority) 220 if err != nil { // authority didn't have a port 221 port = "443" 222 host = authority 223 } 224 if a, err := idna.ToASCII(host); err == nil { 225 host = a 226 } 227 // IPv6 address literal, without a port: 228 if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { 229 return host + ":" + port 230 } 231 return net.JoinHostPort(host, port) 232 } 233 234 // validPseudoPath reports whether v is a valid :path pseudo-header 235 // value. It must be either: 236 // 237 // *) a non-empty string starting with '/' 238 // *) the string '*', for OPTIONS requests. 239 // 240 // For now this is only used a quick check for deciding when to clean 241 // up Opaque URLs before sending requests from the Transport. 242 // See golang.org/issue/16847 243 // 244 // We used to enforce that the path also didn't start with "//", but 245 // Google's GFE accepts such paths and Chrome sends them, so ignore 246 // that part of the spec. See golang.org/issue/19103. 247 func validPseudoPath(v string) bool { 248 return (len(v) > 0 && v[0] == '/') || v == "*" 249 } 250 251 // actualContentLength returns a sanitized version of 252 // req.ContentLength, where 0 actually means zero (not unknown) and -1 253 // means unknown. 254 func actualContentLength(req *http.Request) int64 { 255 if req.Body == nil { 256 return 0 257 } 258 if req.ContentLength != 0 { 259 return req.ContentLength 260 } 261 return -1 262 } 263 264 // shouldSendReqContentLength reports whether the http2.Transport should send 265 // a "content-length" request header. This logic is basically a copy of the net/http 266 // transferWriter.shouldSendContentLength. 267 // The contentLength is the corrected contentLength (so 0 means actually 0, not unknown). 268 // -1 means unknown. 269 func shouldSendReqContentLength(method string, contentLength int64) bool { 270 if contentLength > 0 { 271 return true 272 } 273 if contentLength < 0 { 274 return false 275 } 276 // For zero bodies, whether we send a content-length depends on the method. 277 // It also kinda doesn't matter for http2 either way, with END_STREAM. 278 switch method { 279 case "POST", "PUT", "PATCH": 280 return true 281 default: 282 return false 283 } 284 }