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 value associated with the specified setting identifier,
119      * or {@code defaultValue} if no value is present for the given key.
120      *
121      * @param key the numeric setting identifier
122      * @param defaultValue the value to return if the setting is not present
123      * @return the configured value for the specified key, or {@code defaultValue},
124      * if the key is not found
125      */
126     public long getOrDefault(long key, long defaultValue) {
127         return settings.getOrDefault(key, defaultValue);
128     }
129 
130     /**
131      * Returns the {@code QPACK_MAX_TABLE_CAPACITY} value.
132      *
133      * @return the current QPACK maximum table capacity, or {@code null} if not set
134      */
135     @Nullable
136     public Long qpackMaxTableCapacity() {
137         return get(Http3SettingIdentifier.HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY.id());
138     }
139 
140     /**
141      * Sets the {@code QPACK_MAX_TABLE_CAPACITY} value.
142      *
143      * @param value QPACK maximum table capacity (must be ≥ 0)
144      * @return this instance for method chaining
145      */
146     public Http3Settings qpackMaxTableCapacity(long value) {
147         put(Http3SettingIdentifier.HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY.id(), value);
148         return this;
149     }
150 
151     /**
152      * Returns the {@code MAX_FIELD_SECTION_SIZE} value.
153      *
154      * @return the maximum field section size, or {@code null} if not set
155      */
156     @Nullable
157     public Long maxFieldSectionSize() {
158         return get(Http3SettingIdentifier.HTTP3_SETTINGS_MAX_FIELD_SECTION_SIZE.id());
159     }
160 
161     /**
162      * Sets the {@code MAX_FIELD_SECTION_SIZE} value.
163      *
164      * @param value maximum field section size (must be ≥ 0)
165      * @return this instance for method chaining
166      */
167     public Http3Settings maxFieldSectionSize(long value) {
168         put(Http3SettingIdentifier.HTTP3_SETTINGS_MAX_FIELD_SECTION_SIZE.id(), value);
169         return this;
170     }
171 
172     /**
173      * Returns the {@code QPACK_BLOCKED_STREAMS} value.
174      *
175      * @return the number of blocked streams, or {@code null} if not set
176      */
177     @Nullable
178     public Long qpackBlockedStreams() {
179         return get(Http3SettingIdentifier.HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS.id());
180     }
181 
182     /**
183      * Sets the {@code QPACK_BLOCKED_STREAMS} value.
184      *
185      * @param value number of blocked streams (must be ≥ 0)
186      * @return this instance for method chaining
187      */
188     public Http3Settings qpackBlockedStreams(long value) {
189         put(Http3SettingIdentifier.HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS.id(), value);
190         return this;
191     }
192 
193     /**
194      * Returns whether the {@code ENABLE_CONNECT_PROTOCOL} setting is enabled.
195      *
196      * @return {@code true} if enabled, {@code false} if disabled, or {@code null} if not set
197      */
198     @Nullable
199     public Boolean connectProtocolEnabled() {
200         Long value = get(Http3SettingIdentifier.HTTP3_SETTINGS_ENABLE_CONNECT_PROTOCOL.id());
201         return value == null ? null : TRUE.equals(value);
202     }
203 
204     /**
205      * Sets the {@code ENABLE_CONNECT_PROTOCOL} flag.
206      *
207      * @param enabled whether to enable the CONNECT protocol
208      * @return this instance for method chaining
209      */
210     public Http3Settings enableConnectProtocol(boolean enabled) {
211         put(Http3SettingIdentifier.HTTP3_SETTINGS_ENABLE_CONNECT_PROTOCOL.id(), enabled ? TRUE : FALSE);
212         return this;
213     }
214 
215     /**
216      * Returns whether the {@code H3_DATAGRAM} setting is enabled.
217      *
218      * @return {@code true} if enabled, {@code false} if disabled, or {@code null} if not set
219      */
220     @Nullable
221     public Boolean h3DatagramEnabled() {
222         Long value = get(Http3SettingIdentifier.HTTP3_SETTINGS_H3_DATAGRAM.id());
223         return value == null ? null : TRUE.equals(value);
224     }
225 
226     /**
227      * Sets the {@code H3_DATAGRAM} settings identifier.
228      *
229      * @param enabled whether to enable the H3 Datagram
230      * @return this instance for method chaining
231      */
232     public Http3Settings enableH3Datagram(boolean enabled) {
233         put(Http3SettingIdentifier.HTTP3_SETTINGS_H3_DATAGRAM.id(), enabled ? TRUE : FALSE);
234         return this;
235     }
236 
237     /**
238      * Replaces all current settings with those from another {@link Http3Settings} instance.
239      *
240      * @param http3Settings the source settings (non-null)
241      * @return this instance for method chaining
242      */
243     public Http3Settings putAll(Http3Settings http3Settings) {
244         checkNotNull(http3Settings, "http3Settings");
245         settings.putAll(http3Settings.settings);
246         return this;
247     }
248 
249     /**
250      * Returns a new {@link Http3Settings} instance with default values:
251      * <ul>
252      *   <li>{@code QPACK_MAX_TABLE_CAPACITY} = 0</li>
253      *   <li>{@code QPACK_BLOCKED_STREAMS} = 0</li>
254      *   <li>{@code ENABLE_CONNECT_PROTOCOL} = false</li>
255      *   <li>{@code MAX_FIELD_SECTION_SIZE} = 8192</li>
256      *   <li>{@code H3_DATAGRAM} = false </>
257      * </ul>
258      *
259      * @return a default {@link Http3Settings} instance
260      */
261     public static Http3Settings defaultSettings() {
262         return new Http3Settings()
263                 .qpackMaxTableCapacity(0)
264                 .qpackBlockedStreams(0)
265                 .maxFieldSectionSize(Http3CodecUtils.DEFAULT_MAX_FIELD_SECTION_SIZE)
266                 .enableConnectProtocol(false)
267                 .enableH3Datagram(false);
268     }
269 
270     /**
271      * Returns an iterator over the settings entries in this object.
272      * Each entry’s key is the numeric setting identifier, and the value is its numeric value.
273      *
274      * @return an iterator over immutable {@link Map.Entry} objects
275      */
276     @Override
277     public Iterator<Map.Entry<Long, Long>> iterator() {
278         Iterator<LongObjectMap.PrimitiveEntry<Long>> it = settings.entries().iterator();
279         return new Iterator<Map.Entry<Long, Long>>() {
280             @Override
281             public boolean hasNext() {
282                 return it.hasNext();
283             }
284 
285             @Override
286             public Map.Entry<Long, Long> next() {
287                 LongObjectMap.PrimitiveEntry<Long> entry = it.next();
288                 return new java.util.AbstractMap.SimpleImmutableEntry<>(entry.key(), entry.value());
289             }
290         };
291     }
292 
293     /**
294      * Compares this settings object to another for equality.
295      * Two instances are equal if they contain the same key–value pairs.
296      *
297      * @param o the other object
298      * @return {@code true} if equal, {@code false} otherwise
299      */
300     @Override
301     public boolean equals(Object o) {
302         if (this == o) {
303             return true;
304         }
305         if (!(o instanceof Http3Settings)) {
306             return false;
307         }
308         Http3Settings that = (Http3Settings) o;
309         return settings.equals(that.settings);
310     }
311 
312     /**
313      * Returns the hash code of this settings object, based on its key–value pairs.
314      *
315      * @return the hash code
316      */
317     @Override
318     public int hashCode() {
319         return settings.hashCode();
320     }
321 
322     /**
323      * Returns a string representation of this settings object in the form:
324      * <pre>
325      * Http3Settings{0x1=100, 0x6=16384, 0x7=0}
326      * </pre>
327      *
328      * @return a human-readable string representation of the settings
329      */
330     @Override
331     public String toString() {
332         StringBuilder sb = new StringBuilder("Http3Settings{");
333         boolean first = true;
334         for (LongObjectMap.PrimitiveEntry<Long> e : settings.entries()) {
335             if (!first) {
336                 sb.append(", ");
337             }
338             first = false;
339             sb.append("0x").append(toHexString(e.key())).append('=').append(e.value());
340         }
341         return sb.append('}').toString();
342     }
343 
344     /**
345      * Validates a setting identifier and value pair against HTTP/3.
346      * Note that it can only validate the valid HTTP/3 settings
347      * Does not validate non-standard settings
348      * @param identifier the setting identifier
349      * @param value the setting value
350      * @throws IllegalArgumentException if the identifier or value violates the protocol specification
351      */
352     private static void verifyStandardSetting(Http3SettingIdentifier identifier, Long value) {
353         checkNotNull(value, "value");
354         checkNotNull(identifier, "identifier");
355 
356         switch (identifier) {
357             case HTTP3_SETTINGS_QPACK_MAX_TABLE_CAPACITY:
358             case HTTP3_SETTINGS_QPACK_BLOCKED_STREAMS:
359             case HTTP3_SETTINGS_MAX_FIELD_SECTION_SIZE:
360                 if (value < 0) {
361                     throw new IllegalArgumentException("Setting 0x" + toHexString(identifier.id())
362                             + " invalid: " + value + " (must be >= 0)");
363                 }
364                 break;
365             case HTTP3_SETTINGS_ENABLE_CONNECT_PROTOCOL:
366             case HTTP3_SETTINGS_H3_DATAGRAM:
367                 if (value != 0L && value != 1L) {
368                     throw new IllegalArgumentException(
369                             "Invalid: " + value + "for "
370                                     + Http3SettingIdentifier.valueOf(String.valueOf(identifier))
371                             + " (expected 0 or 1)");
372                 }
373                 break;
374             default:
375                 if (value < 0) {
376                     throw new IllegalArgumentException("Setting 0x"
377                             + toHexString(identifier.id()) + " invalid: " + value);
378                 }
379         }
380     }
381 
382     /**
383      * Allows to handle non-standard settings. By default non-standard settings will be ignore as defined by the
384      * RFC.
385      */
386     public interface NonStandardHttp3SettingsValidator {
387         /**
388          * Validate the setting with the given id and value.
389          *
390          * @param id        the id of the setting
391          * @param value     the value of the setting
392          * @return          {@code true} if the settings is supported, {@code false} otherwise.
393          * @throws IllegalArgumentException if the given {@code value} is not supported for the id.
394          */
395         boolean validate(long id, Long value) throws IllegalArgumentException;
396     }
397 }