github.com/tumi8/quic-go@v0.37.4-tum/http3/headers.go (about) 1 package http3 2 3 import ( 4 "errors" 5 "fmt" 6 "net/http" 7 "net/url" 8 "strconv" 9 "strings" 10 11 "golang.org/x/net/http/httpguts" 12 13 "github.com/quic-go/qpack" 14 ) 15 16 type header struct { 17 // Pseudo header fields defined in RFC 9114 18 Path string 19 Method string 20 Authority string 21 Scheme string 22 Status string 23 // for Extended connect 24 Protocol string 25 // parsed and deduplicated 26 ContentLength int64 27 // all non-pseudo headers 28 Headers http.Header 29 } 30 31 func parseHeaders(headers []qpack.HeaderField, isRequest bool) (header, error) { 32 hdr := header{Headers: make(http.Header, len(headers))} 33 var readFirstRegularHeader, readContentLength bool 34 var contentLengthStr string 35 for _, h := range headers { 36 // field names need to be lowercase, see section 4.2 of RFC 9114 37 if strings.ToLower(h.Name) != h.Name { 38 return header{}, fmt.Errorf("header field is not lower-case: %s", h.Name) 39 } 40 if !httpguts.ValidHeaderFieldValue(h.Value) { 41 return header{}, fmt.Errorf("invalid header field value for %s: %q", h.Name, h.Value) 42 } 43 if h.IsPseudo() { 44 if readFirstRegularHeader { 45 // all pseudo headers must appear before regular header fields, see section 4.3 of RFC 9114 46 return header{}, fmt.Errorf("received pseudo header %s after a regular header field", h.Name) 47 } 48 var isResponsePseudoHeader bool // pseudo headers are either valid for requests or for responses 49 switch h.Name { 50 case ":path": 51 hdr.Path = h.Value 52 case ":method": 53 hdr.Method = h.Value 54 case ":authority": 55 hdr.Authority = h.Value 56 case ":protocol": 57 hdr.Protocol = h.Value 58 case ":scheme": 59 hdr.Scheme = h.Value 60 case ":status": 61 hdr.Status = h.Value 62 isResponsePseudoHeader = true 63 default: 64 return header{}, fmt.Errorf("unknown pseudo header: %s", h.Name) 65 } 66 if isRequest && isResponsePseudoHeader { 67 return header{}, fmt.Errorf("invalid request pseudo header: %s", h.Name) 68 } 69 if !isRequest && !isResponsePseudoHeader { 70 return header{}, fmt.Errorf("invalid response pseudo header: %s", h.Name) 71 } 72 } else { 73 if !httpguts.ValidHeaderFieldName(h.Name) { 74 return header{}, fmt.Errorf("invalid header field name: %q", h.Name) 75 } 76 readFirstRegularHeader = true 77 switch h.Name { 78 case "content-length": 79 // Ignore duplicate Content-Length headers. 80 // Fail if the duplicates differ. 81 if !readContentLength { 82 readContentLength = true 83 contentLengthStr = h.Value 84 } else if contentLengthStr != h.Value { 85 return header{}, fmt.Errorf("contradicting content lengths (%s and %s)", contentLengthStr, h.Value) 86 } 87 default: 88 hdr.Headers.Add(h.Name, h.Value) 89 } 90 } 91 } 92 if len(contentLengthStr) > 0 { 93 // use ParseUint instead of ParseInt, so that parsing fails on negative values 94 cl, err := strconv.ParseUint(contentLengthStr, 10, 63) 95 if err != nil { 96 return header{}, fmt.Errorf("invalid content length: %w", err) 97 } 98 hdr.Headers.Set("Content-Length", contentLengthStr) 99 hdr.ContentLength = int64(cl) 100 } 101 return hdr, nil 102 } 103 104 func requestFromHeaders(headerFields []qpack.HeaderField) (*http.Request, error) { 105 hdr, err := parseHeaders(headerFields, true) 106 if err != nil { 107 return nil, err 108 } 109 // concatenate cookie headers, see https://tools.ietf.org/html/rfc6265#section-5.4 110 if len(hdr.Headers["Cookie"]) > 0 { 111 hdr.Headers.Set("Cookie", strings.Join(hdr.Headers["Cookie"], "; ")) 112 } 113 114 isConnect := hdr.Method == http.MethodConnect 115 // Extended CONNECT, see https://datatracker.ietf.org/doc/html/rfc8441#section-4 116 isExtendedConnected := isConnect && hdr.Protocol != "" 117 if isExtendedConnected { 118 if hdr.Scheme == "" || hdr.Path == "" || hdr.Authority == "" { 119 return nil, errors.New("extended CONNECT: :scheme, :path and :authority must not be empty") 120 } 121 } else if isConnect { 122 if hdr.Path != "" || hdr.Authority == "" { // normal CONNECT 123 return nil, errors.New(":path must be empty and :authority must not be empty") 124 } 125 } else if len(hdr.Path) == 0 || len(hdr.Authority) == 0 || len(hdr.Method) == 0 { 126 return nil, errors.New(":path, :authority and :method must not be empty") 127 } 128 129 var u *url.URL 130 var requestURI string 131 var protocol string 132 133 if isConnect { 134 u = &url.URL{} 135 if isExtendedConnected { 136 u, err = url.ParseRequestURI(hdr.Path) 137 if err != nil { 138 return nil, err 139 } 140 } else { 141 u.Path = hdr.Path 142 } 143 u.Scheme = hdr.Scheme 144 u.Host = hdr.Authority 145 requestURI = hdr.Authority 146 protocol = hdr.Protocol 147 } else { 148 protocol = "HTTP/3.0" 149 u, err = url.ParseRequestURI(hdr.Path) 150 if err != nil { 151 return nil, fmt.Errorf("invalid content length: %w", err) 152 } 153 requestURI = hdr.Path 154 } 155 156 return &http.Request{ 157 Method: hdr.Method, 158 URL: u, 159 Proto: protocol, 160 ProtoMajor: 3, 161 ProtoMinor: 0, 162 Header: hdr.Headers, 163 Body: nil, 164 ContentLength: hdr.ContentLength, 165 Host: hdr.Authority, 166 RequestURI: requestURI, 167 }, nil 168 } 169 170 func hostnameFromRequest(req *http.Request) string { 171 if req.URL != nil { 172 return req.URL.Host 173 } 174 return "" 175 } 176 177 func responseFromHeaders(headerFields []qpack.HeaderField) (*http.Response, error) { 178 hdr, err := parseHeaders(headerFields, false) 179 if err != nil { 180 return nil, err 181 } 182 if hdr.Status == "" { 183 return nil, errors.New("missing status field") 184 } 185 rsp := &http.Response{ 186 Proto: "HTTP/3.0", 187 ProtoMajor: 3, 188 Header: hdr.Headers, 189 ContentLength: hdr.ContentLength, 190 } 191 status, err := strconv.Atoi(hdr.Status) 192 if err != nil { 193 return nil, fmt.Errorf("invalid status code: %w", err) 194 } 195 rsp.StatusCode = status 196 rsp.Status = hdr.Status + " " + http.StatusText(status) 197 return rsp, nil 198 }