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