View Javadoc
1   /*
2    * Copyright 2025 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,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package io.netty.handler.codec.http3;
17  
18  import io.netty.util.collection.LongObjectHashMap;
19  import io.netty.util.collection.LongObjectMap;
20  
21  import javax.annotation.Nullable;
22  import java.util.Iterator;
23  import java.util.Map;
24  
25  import static java.lang.Long.toHexString;
26  import static io.netty.util.internal.ObjectUtil.checkNotNull;
27  
28  /**
29   * Represents a collection of HTTP/3 settings as defined by the
30   * <a href="https://datatracker.ietf.org/doc/html/rfc9114#section-7.2.4">
31   * HTTP/3 specification</a>.
32   *
33   * <p>This class provides type-safe accessors for standard HTTP/3 settings such as:
34   * <ul>
35   *   <li>{@code QPACK_MAX_TABLE_CAPACITY} (0x1)</li>
36   *   <li>{@code MAX_FIELD_SECTION_SIZE} (0x6)</li>
37   *   <li>{@code QPACK_BLOCKED_STREAMS} (0x7)</li>
38   *   <li>{@code ENABLE_CONNECT_PROTOCOL} (0x8)</li>
39   *   <li>{@code H3_DATAGRAM} (0x33)</>
40   * </ul>
41   *
42   * Non-standard settings are ignored
43   * Reserved HTTP/2 setting identifiers are rejected.
44   *
45   */
46  public final class Http3Settings implements Iterable<Map.Entry<Long, Long>> {
47  
48      private final LongObjectMap<Long> settings;
49      final NonStandardHttp3SettingsValidator nonStandardSettingsValidator;
50      private static final Long TRUE = 1L;
51      private static final Long FALSE = 0L;
52  
53      /**
54       * Creates a new instance
55       */
56      public Http3Settings() {
57          // Ignore non-standard settings by default.
58          this((id, v) -> false);
59      }
60  
61      /**
62       * Creates a new instance
63       *
64       * @param nonStandardSettingsValidator the {@link NonStandardHttp3SettingsValidator} to use to check if a specific
65       *                                     setting that is non-standard should be supported or not.
66       */
67      public Http3Settings(NonStandardHttp3SettingsValidator nonStandardSettingsValidator) {
68          this.settings = new LongObjectHashMap<>(Http3SettingIdentifier.values().length);
69          this.nonStandardSettingsValidator = checkNotNull(nonStandardSettingsValidator, "nonStandardSettingsValidator");
70      }
71  
72      /**
73       * Stores a setting value for the specified identifier.
74       * <p>
75       * The key and value are validated according to the HTTP/3 specification.
76       * Reserved HTTP/2 setting identifiers and negative values are not allowed.
77       * Ignore any unknown id/key as per <a href="https://www.rfc-editor.org/rfc/rfc9114.html#section-7.2.4-9>rfc9114</a>
78       * @param key   the numeric setting identifier
79       * @param value the setting value (non-null)
80       * @return the previous value associated with the key, or {@code null} if none
81       * @throws IllegalArgumentException if the key or value is invalid
82       */
83      @Nullable
84      public Long put(long key, Long value) {
85  
86          // When HTTP2 settings identifier present - Throw Error
87          if (Http3CodecUtils.isReservedHttp2Setting(key)) {
88              throw new IllegalArgumentException("Setting is reserved for HTTP/2: " + key);
89          }
90  
91          Http3SettingIdentifier identifier = Http3SettingIdentifier.fromId(key);
92  
93          if (identifier == null) {
94              // When Non-Standard/Unknown settings identifier present check if we should ignore it or not.
95              if (!nonStandardSettingsValidator.validate(key, value)) {
96                  return null;
97              }
98          } else {
99              //Validation
100             verifyStandardSetting(identifier, value);
101         }
102 
103         return settings.put(key, value);
104     }
105 
106     /**
107      * Returns the value of the specified setting identifier.
108      *
109      * @param key the numeric setting identifier
110      * @return the setting value, or {@code null} if not set
111      */
112     @Nullable
113     public Long get(long key) {
114         return settings.get(key);
115     }
116 
117     /**
118      * Returns the {@code QPACK_MAX_TABLE_CAPACITY} value.
119      *
120      * @return the current QPACK maximum table capacity, or {@code null} if not set
121      */
122     @Nullable
123     public Long qpackMaxTableCapacity() {
124         return get(Http3SettingIdentifier.HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY.id());
125     }
126 
127     /**
128      * Sets the {@code QPACK_MAX_TABLE_CAPACITY} value.
129      *
130      * @param value QPACK maximum table capacity (must be ≥ 0)
131      * @return this instance for method chaining
132      */
133     public Http3Settings qpackMaxTableCapacity(long value) {
134         put(Http3SettingIdentifier.HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY.id(), value);
135         return this;
136     }
137 
138     /**
139      * Returns the {@code MAX_FIELD_SECTION_SIZE} value.
140      *
141      * @return the maximum field section size, or {@code null} if not set
142      */
143     @Nullable
144     public Long maxFieldSectionSize() {
145         return get(Http3SettingIdentifier.HTTP3_SETTINGS_MAX_FIELD_SECTION_SIZE.id());
146     }
147 
148     /**
149      * Sets the {@code MAX_FIELD_SECTION_SIZE} value.
150      *
151      * @param value maximum field section size (must be ≥ 0)
152      * @return this instance for method chaining
153      */
154     public Http3Settings maxFieldSectionSize(long value) {
155         put(Http3SettingIdentifier.HTTP3_SETTINGS_MAX_FIELD_SECTION_SIZE.id(), value);
156         return this;
157     }
158 
159     /**
160      * Returns the {@code QPACK_BLOCKED_STREAMS} value.
161      *
162      * @return the number of blocked streams, or {@code null} if not set
163      */
164     @Nullable
165     public Long qpackBlockedStreams() {
166         return get(Http3SettingIdentifier.HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS.id());
167     }
168 
169     /**
170      * Sets the {@code QPACK_BLOCKED_STREAMS} value.
171      *
172      * @param value number of blocked streams (must be ≥ 0)
173      * @return this instance for method chaining
174      */
175     public Http3Settings qpackBlockedStreams(long value) {
176         put(Http3SettingIdentifier.HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS.id(), value);
177         return this;
178     }
179 
180     /**
181      * Returns whether the {@code ENABLE_CONNECT_PROTOCOL} setting is enabled.
182      *
183      * @return {@code true} if enabled, {@code false} if disabled, or {@code null} if not set
184      */
185     @Nullable
186     public Boolean connectProtocolEnabled() {
187         Long value = get(Http3SettingIdentifier.HTTP3_SETTINGS_ENABLE_CONNECT_PROTOCOL.id());
188         return value == null ? null : TRUE.equals(value);
189     }
190 
191     /**
192      * Sets the {@code ENABLE_CONNECT_PROTOCOL} flag.
193      *
194      * @param enabled whether to enable the CONNECT protocol
195      * @return this instance for method chaining
196      */
197     public Http3Settings enableConnectProtocol(boolean enabled) {
198         put(Http3SettingIdentifier.HTTP3_SETTINGS_ENABLE_CONNECT_PROTOCOL.id(), enabled ? TRUE : FALSE);
199         return this;
200     }
201 
202     /**
203      * Returns whether the {@code H3_DATAGRAM} setting is enabled.
204      *
205      * @return {@code true} if enabled, {@code false} if disabled, or {@code null} if not set
206      */
207     @Nullable
208     public Boolean h3DatagramEnabled() {
209         Long value = get(Http3SettingIdentifier.HTTP3_SETTINGS_H3_DATAGRAM.id());
210         return value == null ? null : TRUE.equals(value);
211     }
212 
213     /**
214      * Sets the {@code H3_DATAGRAM} settings identifier.
215      *
216      * @param enabled whether to enable the H3 Datagram
217      * @return this instance for method chaining
218      */
219     public Http3Settings enableH3Datagram(boolean enabled) {
220         put(Http3SettingIdentifier.HTTP3_SETTINGS_H3_DATAGRAM.id(), enabled ? TRUE : FALSE);
221         return this;
222     }
223 
224     /**
225      * Replaces all current settings with those from another {@link Http3Settings} instance.
226      *
227      * @param http3Settings the source settings (non-null)
228      * @return this instance for method chaining
229      */
230     public Http3Settings putAll(Http3Settings http3Settings) {
231         checkNotNull(http3Settings, "http3Settings");
232         settings.putAll(http3Settings.settings);
233         return this;
234     }
235 
236     /**
237      * Returns a new {@link Http3Settings} instance with default values:
238      * <ul>
239      *   <li>{@code QPACK_MAX_TABLE_CAPACITY} = 0</li>
240      *   <li>{@code QPACK_BLOCKED_STREAMS} = 0</li>
241      *   <li>{@code ENABLE_CONNECT_PROTOCOL} = false</li>
242      *   <li>{@code MAX_FIELD_SECTION_SIZE} = unlimited</li>
243      *   <li>{@code H3_DATAGRAM} = false </>
244      * </ul>
245      *
246      * @return a default {@link Http3Settings} instance
247      */
248     public static Http3Settings defaultSettings() {
249         return new Http3Settings()
250                 .qpackMaxTableCapacity(0)
251                 .qpackBlockedStreams(0)
252                 .maxFieldSectionSize(16 * 1024 * 1024)
253                 .enableConnectProtocol(false)
254                 .enableH3Datagram(false);
255     }
256 
257     /**
258      * Returns an iterator over the settings entries in this object.
259      * Each entry’s key is the numeric setting identifier, and the value is its numeric value.
260      *
261      * @return an iterator over immutable {@link Map.Entry} objects
262      */
263     @Override
264     public Iterator<Map.Entry<Long, Long>> iterator() {
265         Iterator<LongObjectMap.PrimitiveEntry<Long>> it = settings.entries().iterator();
266         return new Iterator<Map.Entry<Long, Long>>() {
267             @Override
268             public boolean hasNext() {
269                 return it.hasNext();
270             }
271 
272             @Override
273             public Map.Entry<Long, Long> next() {
274                 LongObjectMap.PrimitiveEntry<Long> entry = it.next();
275                 return new java.util.AbstractMap.SimpleImmutableEntry<>(entry.key(), entry.value());
276             }
277         };
278     }
279 
280     /**
281      * Compares this settings object to another for equality.
282      * Two instances are equal if they contain the same key–value pairs.
283      *
284      * @param o the other object
285      * @return {@code true} if equal, {@code false} otherwise
286      */
287     @Override
288     public boolean equals(Object o) {
289         if (this == o) {
290             return true;
291         }
292         if (!(o instanceof Http3Settings)) {
293             return false;
294         }
295         Http3Settings that = (Http3Settings) o;
296         return settings.equals(that.settings);
297     }
298 
299     /**
300      * Returns the hash code of this settings object, based on its key–value pairs.
301      *
302      * @return the hash code
303      */
304     @Override
305     public int hashCode() {
306         return settings.hashCode();
307     }
308 
309     /**
310      * Returns a string representation of this settings object in the form:
311      * <pre>
312      * Http3Settings{0x1=100, 0x6=16384, 0x7=0}
313      * </pre>
314      *
315      * @return a human-readable string representation of the settings
316      */
317     @Override
318     public String toString() {
319         StringBuilder sb = new StringBuilder("Http3Settings{");
320         boolean first = true;
321         for (LongObjectMap.PrimitiveEntry<Long> e : settings.entries()) {
322             if (!first) {
323                 sb.append(", ");
324             }
325             first = false;
326             sb.append("0x").append(toHexString(e.key())).append('=').append(e.value());
327         }
328         return sb.append('}').toString();
329     }
330 
331     /**
332      * Validates a setting identifier and value pair against HTTP/3.
333      * Note that it can only validate the valid HTTP/3 settings
334      * Does not validate non-standard settings
335      * @param identifier the setting identifier
336      * @param value the setting value
337      * @throws IllegalArgumentException if the identifier or value violates the protocol specification
338      */
339     private static void verifyStandardSetting(Http3SettingIdentifier identifier, Long value) {
340         checkNotNull(value, "value");
341         checkNotNull(identifier, "identifier");
342 
343         switch (identifier) {
344             case HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY:
345             case HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS:
346             case HTTP3_SETTINGS_MAX_FIELD_SECTION_SIZE:
347                 if (value < 0) {
348                     throw new IllegalArgumentException("Setting 0x" + toHexString(identifier.id())
349                             + " invalid: " + value + " (must be >= 0)");
350                 }
351                 break;
352             case HTTP3_SETTINGS_ENABLE_CONNECT_PROTOCOL:
353             case HTTP3_SETTINGS_H3_DATAGRAM:
354                 if (value != 0L && value != 1L) {
355                     throw new IllegalArgumentException(
356                             "Invalid: " + value + "for "
357                                     + Http3SettingIdentifier.valueOf(String.valueOf(identifier))
358                             + " (expected 0 or 1)");
359                 }
360                 break;
361             default:
362                 if (value < 0) {
363                     throw new IllegalArgumentException("Setting 0x"
364                             + toHexString(identifier.id()) + " invalid: " + value);
365                 }
366         }
367     }
368 
369     /**
370      * Allows to handle non-standard settings. By default non-standard settings will be ignore as defined by the
371      * RFC.
372      */
373     public interface NonStandardHttp3SettingsValidator {
374         /**
375          * Validate the setting with the given id and value.
376          *
377          * @param id        the id of the setting
378          * @param value     the value of the setting
379          * @return          {@code true} if the settings is supported, {@code false} otherwise.
380          * @throws IllegalArgumentException if the given {@code value} is not supported for the id.
381          */
382         boolean validate(long id, Long value) throws IllegalArgumentException;
383     }
384 }