|
| 1 | +// Copyright 2025 The Go Authors. All rights reserved. |
| 2 | +// Use of this source code is governed by a BSD-style |
| 3 | +// license that can be found in the LICENSE file. |
| 4 | + |
| 5 | +//go:build go1.24 |
| 6 | + |
| 7 | +package http3 |
| 8 | + |
| 9 | +import ( |
| 10 | + "io" |
| 11 | + "net/http" |
| 12 | + "strconv" |
| 13 | + |
| 14 | + "golang.org/x/net/internal/httpcommon" |
| 15 | +) |
| 16 | + |
| 17 | +// RoundTrip sends a request on the connection. |
| 18 | +func (cc *ClientConn) RoundTrip(req *http.Request) (_ *http.Response, err error) { |
| 19 | + // Each request gets its own QUIC stream. |
| 20 | + st, err := newConnStream(req.Context(), cc.qconn, streamTypeRequest) |
| 21 | + if err != nil { |
| 22 | + return nil, err |
| 23 | + } |
| 24 | + defer func() { |
| 25 | + switch e := err.(type) { |
| 26 | + case nil: |
| 27 | + case *connectionError: |
| 28 | + cc.abort(e) |
| 29 | + case *streamError: |
| 30 | + st.stream.CloseRead() |
| 31 | + st.stream.Reset(uint64(e.code)) |
| 32 | + default: |
| 33 | + st.stream.CloseRead() |
| 34 | + st.stream.Reset(uint64(errH3NoError)) |
| 35 | + } |
| 36 | + }() |
| 37 | + |
| 38 | + // Cancel reads/writes on the stream when the request expires. |
| 39 | + st.stream.SetReadContext(req.Context()) |
| 40 | + st.stream.SetWriteContext(req.Context()) |
| 41 | + |
| 42 | + var encr httpcommon.EncodeHeadersResult |
| 43 | + headers := cc.enc.encode(func(yield func(itype indexType, name, value string)) { |
| 44 | + encr, err = httpcommon.EncodeHeaders(httpcommon.EncodeHeadersParam{ |
| 45 | + Request: req, |
| 46 | + AddGzipHeader: false, // TODO: add when appropriate |
| 47 | + PeerMaxHeaderListSize: 0, |
| 48 | + DefaultUserAgent: "Go-http-client/3", |
| 49 | + }, func(name, value string) { |
| 50 | + // Issue #71374: Consider supporting never-indexed fields. |
| 51 | + yield(mayIndex, name, value) |
| 52 | + }) |
| 53 | + }) |
| 54 | + if err != nil { |
| 55 | + return nil, err |
| 56 | + } |
| 57 | + |
| 58 | + // Write the HEADERS frame. |
| 59 | + st.writeVarint(int64(frameTypeHeaders)) |
| 60 | + st.writeVarint(int64(len(headers))) |
| 61 | + st.Write(headers) |
| 62 | + if err := st.Flush(); err != nil { |
| 63 | + return nil, err |
| 64 | + } |
| 65 | + |
| 66 | + if encr.HasBody { |
| 67 | + // TODO: Send the request body. |
| 68 | + } |
| 69 | + |
| 70 | + // Read the response headers. |
| 71 | + for { |
| 72 | + ftype, err := st.readFrameHeader() |
| 73 | + if err != nil { |
| 74 | + return nil, err |
| 75 | + } |
| 76 | + switch ftype { |
| 77 | + case frameTypeHeaders: |
| 78 | + statusCode, h, err := cc.handleHeaders(st) |
| 79 | + if err != nil { |
| 80 | + return nil, err |
| 81 | + } |
| 82 | + |
| 83 | + if statusCode >= 100 && statusCode < 199 { |
| 84 | + // TODO: Handle 1xx responses. |
| 85 | + continue |
| 86 | + } |
| 87 | + |
| 88 | + // We have the response headers. |
| 89 | + // Set up the response and return it to the caller. |
| 90 | + contentLength, err := parseResponseContentLength(req.Method, statusCode, h) |
| 91 | + if err != nil { |
| 92 | + return nil, err |
| 93 | + } |
| 94 | + resp := &http.Response{ |
| 95 | + Proto: "HTTP/3.0", |
| 96 | + ProtoMajor: 3, |
| 97 | + Header: h, |
| 98 | + StatusCode: statusCode, |
| 99 | + Status: strconv.Itoa(statusCode) + " " + http.StatusText(statusCode), |
| 100 | + ContentLength: contentLength, |
| 101 | + Body: io.NopCloser(nil), // TODO: read the response body |
| 102 | + } |
| 103 | + // TODO: Automatic Content-Type: gzip decoding. |
| 104 | + return resp, nil |
| 105 | + case frameTypePushPromise: |
| 106 | + if err := cc.handlePushPromise(st); err != nil { |
| 107 | + return nil, err |
| 108 | + } |
| 109 | + default: |
| 110 | + if err := st.discardUnknownFrame(ftype); err != nil { |
| 111 | + return nil, err |
| 112 | + } |
| 113 | + } |
| 114 | + } |
| 115 | +} |
| 116 | + |
| 117 | +func parseResponseContentLength(method string, statusCode int, h http.Header) (int64, error) { |
| 118 | + clens := h["Content-Length"] |
| 119 | + if len(clens) == 0 { |
| 120 | + return -1, nil |
| 121 | + } |
| 122 | + |
| 123 | + // We allow duplicate Content-Length headers, |
| 124 | + // but only if they all have the same value. |
| 125 | + for _, v := range clens[1:] { |
| 126 | + if clens[0] != v { |
| 127 | + return -1, &streamError{errH3MessageError, "mismatching Content-Length headers"} |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + // "A server MUST NOT send a Content-Length header field in any response |
| 132 | + // with a status code of 1xx (Informational) or 204 (No Content). |
| 133 | + // A server MUST NOT send a Content-Length header field in any 2xx (Successful) |
| 134 | + // response to a CONNECT request [...]" |
| 135 | + // https://www.rfc-editor.org/rfc/rfc9110#section-8.6-8 |
| 136 | + if (statusCode >= 100 && statusCode < 200) || |
| 137 | + statusCode == 204 || |
| 138 | + (method == "CONNECT" && statusCode >= 200 && statusCode < 300) { |
| 139 | + // This is a protocol violation, but a fairly harmless one. |
| 140 | + // Just ignore the header. |
| 141 | + return -1, nil |
| 142 | + } |
| 143 | + |
| 144 | + contentLen, err := strconv.ParseUint(clens[0], 10, 63) |
| 145 | + if err != nil { |
| 146 | + return -1, &streamError{errH3MessageError, "invalid Content-Length header"} |
| 147 | + } |
| 148 | + return int64(contentLen), nil |
| 149 | +} |
| 150 | + |
| 151 | +func (cc *ClientConn) handleHeaders(st *stream) (statusCode int, h http.Header, err error) { |
| 152 | + haveStatus := false |
| 153 | + cookie := "" |
| 154 | + // Issue #71374: Consider tracking the never-indexed status of headers |
| 155 | + // with the N bit set in their QPACK encoding. |
| 156 | + err = cc.dec.decode(st, func(_ indexType, name, value string) error { |
| 157 | + switch { |
| 158 | + case name == ":status": |
| 159 | + if haveStatus { |
| 160 | + return &streamError{errH3MessageError, "duplicate :status"} |
| 161 | + } |
| 162 | + haveStatus = true |
| 163 | + statusCode, err = strconv.Atoi(value) |
| 164 | + if err != nil { |
| 165 | + return &streamError{errH3MessageError, "invalid :status"} |
| 166 | + } |
| 167 | + case name[0] == ':': |
| 168 | + // "Endpoints MUST treat a request or response |
| 169 | + // that contains undefined or invalid |
| 170 | + // pseudo-header fields as malformed." |
| 171 | + // https://www.rfc-editor.org/rfc/rfc9114.html#section-4.3-3 |
| 172 | + return &streamError{errH3MessageError, "undefined pseudo-header"} |
| 173 | + case name == "cookie": |
| 174 | + // "If a decompressed field section contains multiple cookie field lines, |
| 175 | + // these MUST be concatenated into a single byte string [...]" |
| 176 | + // using the two-byte delimiter of "; "'' |
| 177 | + // https://www.rfc-editor.org/rfc/rfc9114.html#section-4.2.1-2 |
| 178 | + if cookie == "" { |
| 179 | + cookie = value |
| 180 | + } else { |
| 181 | + cookie += "; " + value |
| 182 | + } |
| 183 | + default: |
| 184 | + if h == nil { |
| 185 | + h = make(http.Header) |
| 186 | + } |
| 187 | + // TODO: Use a per-connection canonicalization cache as we do in HTTP/2. |
| 188 | + // Maybe we could put this in the QPACK decoder and have it deliver |
| 189 | + // pre-canonicalized headers to us here? |
| 190 | + cname := httpcommon.CanonicalHeader(name) |
| 191 | + // TODO: Consider using a single []string slice for all headers, |
| 192 | + // as we do in the HTTP/1 and HTTP/2 cases. |
| 193 | + // This is a bit tricky, since we don't know the number of headers |
| 194 | + // at the start of decoding. Perhaps it's worth doing a two-pass decode, |
| 195 | + // or perhaps we should just allocate header value slices in |
| 196 | + // reasonably-sized chunks. |
| 197 | + h[cname] = append(h[cname], value) |
| 198 | + } |
| 199 | + return nil |
| 200 | + }) |
| 201 | + if !haveStatus { |
| 202 | + // "[The :status] pseudo-header field MUST be included in all responses [...]" |
| 203 | + // https://www.rfc-editor.org/rfc/rfc9114.html#section-4.3.2-1 |
| 204 | + err = errH3MessageError |
| 205 | + } |
| 206 | + if cookie != "" { |
| 207 | + if h == nil { |
| 208 | + h = make(http.Header) |
| 209 | + } |
| 210 | + h["Cookie"] = []string{cookie} |
| 211 | + } |
| 212 | + if err := st.endFrame(); err != nil { |
| 213 | + return 0, nil, err |
| 214 | + } |
| 215 | + return statusCode, h, err |
| 216 | +} |
| 217 | + |
| 218 | +func (cc *ClientConn) handlePushPromise(st *stream) error { |
| 219 | + // "A client MUST treat receipt of a PUSH_PROMISE frame that contains a |
| 220 | + // larger push ID than the client has advertised as a connection error of H3_ID_ERROR." |
| 221 | + // https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.5-5 |
| 222 | + return &connectionError{ |
| 223 | + code: errH3IDError, |
| 224 | + message: "PUSH_PROMISE received when no MAX_PUSH_ID has been sent", |
| 225 | + } |
| 226 | +} |
0 commit comments