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  
21  import java.util.function.BiConsumer;
22  
23  import static io.netty.handler.codec.http3.Http3Headers.PseudoHeaderName.AUTHORITY;
24  import static io.netty.handler.codec.http3.Http3Headers.PseudoHeaderName.METHOD;
25  import static io.netty.handler.codec.http3.Http3Headers.PseudoHeaderName.PATH;
26  import static io.netty.handler.codec.http3.Http3Headers.PseudoHeaderName.PROTOCOL;
27  import static io.netty.handler.codec.http3.Http3Headers.PseudoHeaderName.SCHEME;
28  import static io.netty.handler.codec.http3.Http3Headers.PseudoHeaderName.STATUS;
29  import static io.netty.handler.codec.http3.Http3Headers.PseudoHeaderName.getPseudoHeader;
30  import static io.netty.handler.codec.http3.Http3Headers.PseudoHeaderName.hasPseudoHeaderFormat;
31  
32  /**
33   * {@link BiConsumer} that does add header names and values to
34   * {@link Http3Headers} while also validate these.
35   */
36  final class Http3HeadersSink implements BiConsumer<CharSequence, CharSequence> {
37      private final Http3Headers headers;
38      private final long maxHeaderListSize;
39      private final boolean validate;
40      private final boolean trailer;
41      private long headersLength;
42      private boolean exceededMaxLength;
43      private Http3HeadersValidationException validationException;
44      private HeaderType previousType;
45      private boolean request;
46      private int receivedPseudoHeaders;
47  
48      Http3HeadersSink(Http3Headers headers, long maxHeaderListSize, boolean validate, boolean trailer) {
49          this.headers = headers;
50          this.maxHeaderListSize = maxHeaderListSize;
51          this.validate = validate;
52          this.trailer = trailer;
53      }
54  
55      /**
56       * This method must be called after the sink is used.
57       */
58      void finish() throws Http3HeadersValidationException, Http3Exception {
59          if (exceededMaxLength) {
60              throw new Http3Exception(Http3ErrorCode.H3_EXCESSIVE_LOAD,
61                      String.format("Header size exceeded max allowed size (%d)", maxHeaderListSize));
62          }
63          if (validationException != null) {
64              throw validationException;
65          }
66          if (validate) {
67              if (trailer) {
68                  if (receivedPseudoHeaders != 0) {
69                      // Trailers must not have pseudo headers.
70                      throw new Http3HeadersValidationException("Pseudo-header(s) included in trailers.");
71                  }
72                  return;
73              }
74  
75              // Validate that all mandatory pseudo-headers are included.
76              if (request) {
77                  CharSequence method = headers.method();
78                  // fast-path
79                  if (HttpMethod.CONNECT.asciiName().contentEqualsIgnoreCase(method)) {
80                      // Check if this is an Extended CONNECT request (RFC 9220)
81                      // Extended CONNECT includes the :protocol pseudo-header
82                      if ((receivedPseudoHeaders & PROTOCOL.getFlag()) != 0) {
83                          // Extended CONNECT (RFC 9220) requires:
84                          // - :method
85                          // - :scheme
86                          // - :authority
87                          // - :path
88                          // - :protocol
89                          final int requiredPseudoHeaders = METHOD.getFlag() | SCHEME.getFlag() |
90                                                           AUTHORITY.getFlag() | PATH.getFlag() | PROTOCOL.getFlag();
91                          if (receivedPseudoHeaders != requiredPseudoHeaders) {
92                              throw new Http3HeadersValidationException(
93                                      "Not all mandatory pseudo-headers included for Extended CONNECT.");
94                          }
95                      } else {
96                          // Regular CONNECT (RFC 9114) requires:
97                          // - :method
98                          // - :authority
99                          final int requiredPseudoHeaders = METHOD.getFlag() | AUTHORITY.getFlag();
100                         if (receivedPseudoHeaders != requiredPseudoHeaders) {
101                             throw new Http3HeadersValidationException("Not all mandatory pseudo-headers included.");
102                         }
103                     }
104                 } else if (HttpMethod.OPTIONS.asciiName().contentEqualsIgnoreCase(method)) {
105                     // See:
106                     //
107                     // https://www.rfc-editor.org/rfc/rfc9114.html#section-4.3.1
108                     // https://www.rfc-editor.org/rfc/rfc9110#section-7.1
109                     // - :method
110                     // - :scheme
111                     // - :authority
112                     // - :path
113                     final int requiredPseudoHeaders = METHOD.getFlag() | SCHEME.getFlag() | PATH.getFlag();
114                     if ((receivedPseudoHeaders & requiredPseudoHeaders) != requiredPseudoHeaders ||
115                             (!authorityOrHostHeaderReceived() && !"*".contentEquals(headers.path()))) {
116                         throw new Http3HeadersValidationException("Not all mandatory pseudo-headers included.");
117                     }
118                 } else {
119                     // For other requests we must include:
120                     // - :method
121                     // - :scheme
122                     // - :authority
123                     // - :path
124                     final int requiredPseudoHeaders = METHOD.getFlag() | SCHEME.getFlag() | PATH.getFlag();
125                     if ((receivedPseudoHeaders & requiredPseudoHeaders) != requiredPseudoHeaders ||
126                         !authorityOrHostHeaderReceived()) {
127                         throw new Http3HeadersValidationException("Not all mandatory pseudo-headers included.");
128                     }
129                 }
130             } else {
131                 // For responses we must include:
132                 // - :status
133                 if (receivedPseudoHeaders != STATUS.getFlag()) {
134                     throw new Http3HeadersValidationException("Not all mandatory pseudo-headers included.");
135                 }
136             }
137         }
138     }
139 
140     /**
141      * Find host header field in case the :authority pseudo header is not specified.
142      * See:
143      * https://www.rfc-editor.org/rfc/rfc9110#section-7.2
144      */
145     private boolean authorityOrHostHeaderReceived() {
146         return (receivedPseudoHeaders & AUTHORITY.getFlag()) == AUTHORITY.getFlag() ||
147                 headers.contains(HttpHeaderNames.HOST);
148     }
149 
150     @Override
151     public void accept(CharSequence name, CharSequence value) {
152         headersLength += QpackHeaderField.sizeOf(name, value);
153         exceededMaxLength |= headersLength > maxHeaderListSize;
154 
155         if (exceededMaxLength || validationException != null) {
156             // We don't store the header since we've already failed validation requirements.
157             return;
158         }
159 
160         if (validate) {
161             try {
162                  validate(headers, name);
163             } catch (Http3HeadersValidationException ex) {
164                 validationException = ex;
165                 return;
166             }
167         }
168 
169         headers.add(name, value);
170     }
171 
172     private void validate(Http3Headers headers, CharSequence name) {
173         if (hasPseudoHeaderFormat(name)) {
174             if (previousType == HeaderType.REGULAR_HEADER) {
175                 throw new Http3HeadersValidationException(
176                         String.format("Pseudo-header field '%s' found after regular header.", name));
177             }
178 
179             final Http3Headers.PseudoHeaderName pseudoHeader = getPseudoHeader(name);
180             if (pseudoHeader == null) {
181                 throw new Http3HeadersValidationException(
182                         String.format("Invalid HTTP/3 pseudo-header '%s' encountered.", name));
183             }
184             if ((receivedPseudoHeaders & pseudoHeader.getFlag()) != 0) {
185                 // There can't be any duplicates for pseudy header names.
186                 throw new Http3HeadersValidationException(
187                         String.format("Pseudo-header field '%s' exists already.", name));
188             }
189             receivedPseudoHeaders |= pseudoHeader.getFlag();
190 
191             final HeaderType currentHeaderType = pseudoHeader.isRequestOnly() ?
192                     HeaderType.REQUEST_PSEUDO_HEADER : HeaderType.RESPONSE_PSEUDO_HEADER;
193             request = pseudoHeader.isRequestOnly();
194             previousType = currentHeaderType;
195         } else {
196             previousType = HeaderType.REGULAR_HEADER;
197         }
198     }
199 
200     private enum HeaderType {
201         REGULAR_HEADER,
202         REQUEST_PSEUDO_HEADER,
203         RESPONSE_PSEUDO_HEADER
204     }
205 }