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