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