View Javadoc
1   /*
2    * Copyright 2020 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.http3;
17  
18  import io.netty.handler.codec.http.HttpHeaderNames;
19  import io.netty.handler.codec.http.HttpMethod;
20  import io.netty.util.AsciiString;
21  
22  import java.util.List;
23  import java.util.function.BiConsumer;
24  
25  import static io.netty.handler.codec.http3.Http3Headers.PseudoHeaderName.AUTHORITY;
26  import static io.netty.handler.codec.http3.Http3Headers.PseudoHeaderName.METHOD;
27  import static io.netty.handler.codec.http3.Http3Headers.PseudoHeaderName.PATH;
28  import static io.netty.handler.codec.http3.Http3Headers.PseudoHeaderName.PROTOCOL;
29  import static io.netty.handler.codec.http3.Http3Headers.PseudoHeaderName.SCHEME;
30  import static io.netty.handler.codec.http3.Http3Headers.PseudoHeaderName.STATUS;
31  import static io.netty.handler.codec.http3.Http3Headers.PseudoHeaderName.getPseudoHeader;
32  import static io.netty.handler.codec.http3.Http3Headers.PseudoHeaderName.hasPseudoHeaderFormat;
33  
34  /**
35   * {@link BiConsumer} that does add header names and values to
36   * {@link Http3Headers} while also validate these.
37   */
38  final class Http3HeadersSink implements BiConsumer<CharSequence, CharSequence> {
39      private final Http3Headers headers;
40      private final long maxHeaderListSize;
41      private final boolean validate;
42      private final boolean trailer;
43      private long headersLength;
44      private boolean exceededMaxLength;
45      private Http3HeadersValidationException validationException;
46      private HeaderType previousType;
47      private boolean request;
48      private int receivedPseudoHeaders;
49  
50      Http3HeadersSink(Http3Headers headers, long maxHeaderListSize, boolean validate, boolean trailer) {
51          this.headers = headers;
52          this.maxHeaderListSize = maxHeaderListSize;
53          this.validate = validate;
54          this.trailer = trailer;
55      }
56  
57      /**
58       * This method must be called after the sink is used.
59       */
60      void finish() throws Http3HeadersValidationException, Http3Exception {
61          if (exceededMaxLength) {
62              throw new Http3Exception(Http3ErrorCode.H3_EXCESSIVE_LOAD,
63                      String.format("Header size exceeded max allowed size (%d)", maxHeaderListSize));
64          }
65          if (validationException != null) {
66              throw validationException;
67          }
68          if (validate) {
69              if (trailer) {
70                  if (receivedPseudoHeaders != 0) {
71                      // Trailers must not have pseudo headers.
72                      throw new Http3HeadersValidationException("Pseudo-header(s) included in trailers.");
73                  }
74                  return;
75              }
76  
77              // Validate that all mandatory pseudo-headers are included.
78              if (request) {
79                  validateAuthorityAndHost();
80                  CharSequence method = headers.method();
81                  // fast-path
82                  if (HttpMethod.CONNECT.asciiName().contentEqualsIgnoreCase(method)) {
83                      // Check if this is an Extended CONNECT request (RFC 9220)
84                      // Extended CONNECT includes the :protocol pseudo-header
85                      if ((receivedPseudoHeaders & PROTOCOL.getFlag()) != 0) {
86                          // Extended CONNECT (RFC 9220) requires:
87                          // - :method
88                          // - :scheme
89                          // - :authority
90                          // - :path
91                          // - :protocol
92                          final int requiredPseudoHeaders = METHOD.getFlag() | SCHEME.getFlag() |
93                                                           AUTHORITY.getFlag() | PATH.getFlag() | PROTOCOL.getFlag();
94                          if (receivedPseudoHeaders != requiredPseudoHeaders) {
95                              throw new Http3HeadersValidationException(
96                                      "Not all mandatory pseudo-headers included for Extended CONNECT.");
97                          }
98                      } else {
99                          // Regular CONNECT (RFC 9114) requires:
100                         // - :method
101                         // - :authority
102                         final int requiredPseudoHeaders = METHOD.getFlag() | AUTHORITY.getFlag();
103                         if (receivedPseudoHeaders != requiredPseudoHeaders) {
104                             throw new Http3HeadersValidationException("Not all mandatory pseudo-headers included.");
105                         }
106                     }
107                 } else if (HttpMethod.OPTIONS.asciiName().contentEqualsIgnoreCase(method)) {
108                     // See:
109                     //
110                     // https://www.rfc-editor.org/rfc/rfc9114.html#section-4.3.1
111                     // https://www.rfc-editor.org/rfc/rfc9110#section-7.1
112                     // - :method
113                     // - :scheme
114                     // - :authority
115                     // - :path
116                     final int requiredPseudoHeaders = METHOD.getFlag() | SCHEME.getFlag() | PATH.getFlag();
117                     if ((receivedPseudoHeaders & requiredPseudoHeaders) != requiredPseudoHeaders ||
118                             (!authorityOrHostHeaderReceived() && !"*".contentEquals(headers.path()))) {
119                         throw new Http3HeadersValidationException("Not all mandatory pseudo-headers included.");
120                     }
121                 } else {
122                     // For other requests we must include:
123                     // - :method
124                     // - :scheme
125                     // - :authority
126                     // - :path
127                     final int requiredPseudoHeaders = METHOD.getFlag() | SCHEME.getFlag() | PATH.getFlag();
128                     if ((receivedPseudoHeaders & requiredPseudoHeaders) != requiredPseudoHeaders ||
129                         !authorityOrHostHeaderReceived()) {
130                         throw new Http3HeadersValidationException("Not all mandatory pseudo-headers included.");
131                     }
132                 }
133             } else {
134                 // For responses we must include:
135                 // - :status
136                 if (receivedPseudoHeaders != STATUS.getFlag()) {
137                     throw new Http3HeadersValidationException("Not all mandatory pseudo-headers included.");
138                 }
139             }
140         }
141     }
142 
143     /**
144      * https://www.rfc-editor.org/rfc/rfc9114#section-4.3.1
145      * the request MUST contain either an :authority pseudo-header field or a Host header field.
146      * If both fields are present, they MUST contain the same value.
147      * https://datatracker.ietf.org/doc/html/rfc9110#section-5.3
148      * a sender MUST NOT generate multiple field lines with the same name ...
149      * unless that field's definition allows ... a comma-separated list
150      */
151     private void validateAuthorityAndHost() {
152         if (!headers.contains(HttpHeaderNames.HOST)) {
153             return;
154         }
155         CharSequence authority = headers.authority();
156         List<CharSequence> hosts = headers.getAll(HttpHeaderNames.HOST);
157         CharSequence expected = authority != null ? authority : hosts.get(0);
158         for (int i = 0; i < hosts.size(); i++) {
159             if (!AsciiString.contentEqualsIgnoreCase(expected, hosts.get(i))) {
160                 throw new Http3HeadersValidationException(authority != null ?
161                         "Conflicting ':authority' pseudo-header and 'host' header field." :
162                         "Conflicting 'host' header fields.");
163             }
164         }
165     }
166 
167     /**
168      * Find host header field in case the :authority pseudo header is not specified.
169      * See:
170      * https://www.rfc-editor.org/rfc/rfc9110#section-7.2
171      */
172     private boolean authorityOrHostHeaderReceived() {
173         return (receivedPseudoHeaders & AUTHORITY.getFlag()) == AUTHORITY.getFlag() ||
174                 headers.contains(HttpHeaderNames.HOST);
175     }
176 
177     @Override
178     public void accept(CharSequence name, CharSequence value) {
179         headersLength += QpackHeaderField.sizeOf(name, value);
180         exceededMaxLength |= headersLength > maxHeaderListSize;
181 
182         if (exceededMaxLength || validationException != null) {
183             // We don't store the header since we've already failed validation requirements.
184             return;
185         }
186 
187         if (validate) {
188             try {
189                  validate(headers, name);
190             } catch (Http3HeadersValidationException ex) {
191                 validationException = ex;
192                 return;
193             }
194         }
195 
196         headers.add(name, value);
197     }
198 
199     private void validate(Http3Headers headers, CharSequence name) {
200         if (hasPseudoHeaderFormat(name)) {
201             if (previousType == HeaderType.REGULAR_HEADER) {
202                 throw new Http3HeadersValidationException(
203                         String.format("Pseudo-header field '%s' found after regular header.", name));
204             }
205 
206             final Http3Headers.PseudoHeaderName pseudoHeader = getPseudoHeader(name);
207             if (pseudoHeader == null) {
208                 throw new Http3HeadersValidationException(
209                         String.format("Invalid HTTP/3 pseudo-header '%s' encountered.", name));
210             }
211             if ((receivedPseudoHeaders & pseudoHeader.getFlag()) != 0) {
212                 // There can't be any duplicates for pseudy header names.
213                 throw new Http3HeadersValidationException(
214                         String.format("Pseudo-header field '%s' exists already.", name));
215             }
216             receivedPseudoHeaders |= pseudoHeader.getFlag();
217 
218             final HeaderType currentHeaderType = pseudoHeader.isRequestOnly() ?
219                     HeaderType.REQUEST_PSEUDO_HEADER : HeaderType.RESPONSE_PSEUDO_HEADER;
220             request = pseudoHeader.isRequestOnly();
221             previousType = currentHeaderType;
222         } else {
223             previousType = HeaderType.REGULAR_HEADER;
224         }
225     }
226 
227     private enum HeaderType {
228         REGULAR_HEADER,
229         REQUEST_PSEUDO_HEADER,
230         RESPONSE_PSEUDO_HEADER
231     }
232 }