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 }